Skip to content

Unified View System

Overview

The Unified View System is CRED's widget-based page layout engine. It replaces hardcoded page layouts with customizable, data-driven views composed of reusable widgets. Users can rearrange, show/hide, and resize widgets on any view-system-enabled page without code changes.

Feature Flag

The view system is gated behind FeatureFlag.VIEW_SYSTEM. Feature flags are disabled on develop and enabled per-user in production via PostHog.

Key benefits:

  • User customization — drag-and-drop reordering, visibility toggles, width control
  • Consistent rendering — every page uses the same ViewRenderer + WidgetRenderer pipeline
  • Widget reuse — generic widgets (charts, tables, news) work across entity types
  • Scoped views — GLOBAL defaults, WORKSPACE overrides, PERSONAL customizations
  • Shareable links — generate public links to any view with analytics tracking

Architecture

Full-Stack Overview

┌──────────────────────────────────────────────────────────────────────────┐
│                         Full-Stack Architecture                          │
├──────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌─────────────────── Frontend (cred-web-commercial) ─────────────────┐ │
│   │                                                                     │ │
│   │   FeatureGatedPage (FeatureFlag.VIEW_SYSTEM)                        │ │
│   │        │                                                            │ │
│   │        ▼                                                            │ │
│   │   ViewRenderer (viewTypeCode + entityId)                            │ │
│   │        │                                                            │ │
│   │        ├── ViewContextProvider (entityType + entityId)               │ │
│   │        │                                                            │ │
│   │        └── WidgetRenderer (per-widget error boundary)               │ │
│   │                 │                                                   │ │
│   │                 ▼                                                   │ │
│   │            WidgetRegistry (code → lazy component)                   │ │
│   │                                                                     │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
│                              │                                           │
│                              │ resolvedView(viewTypeCode)                │
│                              ▼                                           │
│   ┌──────────────── Apollo Federation Gateway ─────────────────────────┐ │
│   │                      graphql_router                                 │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
│                              │                                           │
│                              ▼                                           │
│   ┌─────────────────── Backend (cred-api-commercial) ──────────────────┐ │
│   │                                                                     │ │
│   │   ViewResolver (GraphQL queries + mutations)                        │ │
│   │        │                                                            │ │
│   │        ▼                                                            │ │
│   │   Use Cases (ResolveView, CreateView, UpdateViewWidgets, etc.)      │ │
│   │        │                                                            │ │
│   │        ▼                                                            │ │
│   │   Repositories (View, ViewWidget, WidgetDefinition, ViewLink)       │ │
│   │        │                                                            │ │
│   │        ▼                                                            │ │
│   │   PostgreSQL (4 core tables + 1 link table)                         │ │
│   │                                                                     │ │
│   └─────────────────────────────────────────────────────────────────────┘ │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

Backend (cred-api-commercial)

The backend owns the data model and business logic:

  • Domain layer (src/domain/view/) — entities, use cases, repository interfaces, enums
  • Data layer (src/data/models/view/) — Knex/Objection implementations
  • GraphQL layer (src/graphql-api/view/) — resolvers, types, inputs
  • Migrations (src/data/migrations/) — schema and seed data

Frontend (cred-web-commercial)

The frontend owns rendering and interaction:

  • View System module (libs/shared/src/view-system/) — all view system code
  • ViewRenderer — fetches resolved view, builds grid layout, handles drag-and-drop
  • WidgetRenderer — looks up widget in registry, merges config, wraps in error boundary
  • WidgetRegistry — singleton mapping widget codes to lazy-loaded React components

View Resolution Chain

When a page loads, the system resolves the best view for the current user:

┌────────────────────────────────────────────────────────────┐
│                  View Resolution Chain                       │
├────────────────────────────────────────────────────────────┤
│                                                             │
│   Request: viewTypeCode                                     │
│        │                                                    │
│        ▼                                                    │
│   PERSONAL view for this user?                              │
│        ├── YES → Return PERSONAL view                       │
│        │                                                    │
│        └── NO                                               │
│             │                                               │
│             ▼                                               │
│        WORKSPACE view for user's workspace?                 │
│             ├── YES → Return WORKSPACE view                 │
│             │                                               │
│             └── NO                                          │
│                  │                                          │
│                  ▼                                          │
│             Return GLOBAL view (system default)             │
│                                                             │
└────────────────────────────────────────────────────────────┘
Priority Scope Description
1 (highest) PERSONAL User's own customized layout
2 WORKSPACE Workspace-level default
3 (lowest) GLOBAL System default (seeded in migrations)

The ResolveViewUseCase queries each scope in order and returns the first match. GLOBAL views always exist (seeded during deployment), so resolution never fails.


Entities

ViewTypeDefinition

Defines a page type in the system. Each represents a distinct page that can host widgets.

Field Description
code Unique identifier (e.g., company_overview)
name Display name
entityTypeCode Entity type this page is for (COMPANY, PERSON, DEAL, REGION, or NULL for non-entity pages)
routePattern URL pattern (e.g., /companies/[id]/overview)
isSystemView true for built-in page types

Registered view types:

Code Entity Type Route Pattern
home_dashboard /
company_overview COMPANY /companies/[id]/overview
company_audience COMPANY /companies/[id]/audience
company_financial COMPANY /companies/[id]/financial
company_marketing COMPANY /companies/[id]/marketing
person_overview PERSON /people/[id]/overview
deal_overview DEAL /deals/[id]/overview
industry_overview /industries/[type]/[id]/overview
region_overview REGION /regions/[id]/overview
report /reports/[id]

WidgetDefinition

Defines a widget type — the blueprint for a widget that can be placed on views.

Field Description
code Unique identifier (e.g., company.about, generic.chart)
name Display name
widgetType SECTION, CHART, TABLE_VIEW, LIST_VIEW, or TITLE
applicableEntityTypes Which entity types this widget works on (["*"] for all)
applicableViewTypes Which view types this widget can appear on (["*"] for all)
defaultWidth Default grid width (FULL, HALF, THIRD, TWO_THIRDS, QUARTER, THREE_QUARTERS)
defaultConfig JSONB default configuration
category Grouping key (e.g., generic, company, dashboard)
isSystemWidget true for built-in widgets

View

A layout instance — a specific arrangement of widgets for a page type.

Field Description
viewTypeCode Which page type this view is for
scope GLOBAL, WORKSPACE, PERSONAL, or SHARED
name User-editable name
config View-level JSONB config (connectedAccountIds, date range, etc.)
createdByUserId Owner (for PERSONAL views)
workspaceId Workspace (for WORKSPACE views)

ViewWidget

A widget instance within a view — the actual placement of a widget with specific settings.

Field Description
viewId Parent view
widgetCode Which WidgetDefinition this instance uses
sortOrder Display order (1, 2, 3, ...)
visible Whether the widget is shown
width Grid width (overrides WidgetDefinition default)
collapsed Whether the widget renders collapsed initially
useViewContext Whether to inject entity context (entityType + entityId)
config JSONB config override (deep-merged with WidgetDefinition.defaultConfig)

A shareable link to a view with analytics tracking.

Field Description
viewId Parent view FK
token Unique UUID token for public access
isActive Whether the link is active (can be revoked)
expiresAt Optional expiration date
viewCount Total number of times the link has been accessed

Database Schema

Entity Relationships

┌──────────────────────────────────────────────────────────────────────┐
│                         View System Schema                            │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   ViewTypeDefinition                                                 │
│   ├── 1:N  View           (one page type → many layout instances)    │
│                                                                      │
│   WidgetDefinition                                                   │
│   ├── 1:N  ViewWidget     (one widget blueprint → many placements)   │
│                                                                      │
│   View                                                               │
│   ├── 1:N  ViewWidget     (one layout → many widget instances)       │
│   └── 1:N  ViewLink       (one layout → many shareable links)        │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

Width Enum Values

Width Grid Columns Fraction
FULL 12 100%
THREE_QUARTERS 9 75%
TWO_THIRDS 8 66%
HALF 6 50%
THIRD 4 33%
QUARTER 3 25%

Widget Development Guide

Step 1: Create the Widget Component (Frontend)

Create a new file in the view system widgets directory:

// libs/shared/src/view-system/widgets/sections/company/new-feature-widget.tsx

import { type WidgetComponentProps } from "../../../types";
import { WidgetType, WidgetWidth } from "../../../enums";
import type { WidgetConfig } from "../../../registry/types";

export const widgetConfig: WidgetConfig = {
  code: "company.new_feature",
  name: "New Feature",
  description: "Displays the new feature data",
  widgetType: WidgetType.SECTION,
  applicableEntityTypes: ["COMPANY"],
  applicableViewTypes: ["company_overview"],
  defaultWidth: WidgetWidth.FULL,
  defaultVisible: true,
  category: "company",
  tags: [],
};

export default function NewFeatureWidget({ widget, config, context }: WidgetComponentProps) {
  const companyId = context?.companyId;
  // Fetch data and render
  return <div>...</div>;
}

Step 2: Register in Widget Manifest (Frontend)

Add the widget to register-builtin-widgets.ts:

// libs/shared/src/view-system/registry/register-builtin-widgets.ts

import { widgetConfig as newFeatureConfig } from "../widgets/sections/company/new-feature-widget";

const WIDGET_MANIFEST = [
  // ... existing widgets
  { config: newFeatureConfig, loader: () => import("../widgets/sections/company/new-feature-widget") },
];

Step 3: Seed WidgetDefinition (Backend Migration)

Create a migration in cred-api-commercial:

// src/data/migrations/YYYYMMDDHHMMSS_seed-company-new-feature-widget-definition.ts
import type { Knex } from "knex";

export async function up(knex: Knex): Promise<void> {
  await knex("WidgetDefinition").insert({
    code: "company.new_feature",
    name: "New Feature",
    description: "Displays the new feature data",
    widgetType: "SECTION",
    applicableEntityTypes: JSON.stringify(["COMPANY"]),
    applicableViewTypes: JSON.stringify(["company_overview"]),
    componentPath: "widgets/sections/company/NewFeatureWidget",
    defaultWidth: "FULL",
    defaultVisible: true,
    category: "company",
    tags: JSON.stringify([]),
    isSystemWidget: true,
  });
}

export async function down(knex: Knex): Promise<void> {
  await knex("WidgetDefinition").where({ code: "company.new_feature" }).del();
}

Step 4: Add to Default View (Backend Migration)

If the widget should appear on a default (GLOBAL) view:

// In the same or a separate migration
const [view] = await knex("View")
  .where({ viewTypeCode: "company_overview", scope: "GLOBAL" })
  .select("id");

const maxSort = await knex("ViewWidget")
  .where({ viewId: view.id })
  .max("sortOrder as max")
  .first();

await knex("ViewWidget").insert({
  viewId: view.id,
  widgetCode: "company.new_feature",
  sortOrder: (maxSort?.max ?? 0) + 1,
  visible: true,
  width: "FULL",
  collapsed: false,
  useViewContext: true,
  config: JSON.stringify({}),
});

Step 5: Regenerate GraphQL Types (Frontend)

Run codegen in cred-web-commercial:

bun run gql

Dynamic / Prefix-Based Widgets

For widgets that reuse the same component with different configurations (e.g., generic.chart rendering different chart templates):

export const widgetConfig: WidgetConfig = {
  code: "generic.chart",
  // ...
  dynamic: {
    prefix: "chart:",
    configKey: "templateId",
    formatName: (suffix) => `Chart: ${suffix}`,
  },
};

With this pattern, a ViewWidget with widgetCode: "generic.chart" and config: { templateId: "financial.revenue_by_segment" } will render the chart component with that specific template.


Page Migration Guide

How to migrate an existing hardcoded page to the view system.

Step 1: Ensure ViewTypeDefinition Exists

Check the ViewTypeDefinition table for your page's code. If it doesn't exist, create a seed migration:

await knex("ViewTypeDefinition").insert({
  code: "my_page",
  name: "My Page",
  entityTypeCode: "COMPANY", // or NULL for non-entity pages
  routePattern: "/my-page/[id]",
  isSystemView: true,
});

Step 2: Create Widget Definitions

For each section of the legacy page, create a WidgetDefinition (see Widget Development Guide above).

Step 3: Seed the GLOBAL Default View

Create a migration that inserts a GLOBAL view with the default widget layout:

const [view] = await knex("View")
  .insert({
    name: "My Page - Default",
    viewTypeCode: "my_page",
    scope: "GLOBAL",
    config: JSON.stringify({}),
    tags: JSON.stringify([]),
  })
  .returning("id");

await knex("ViewWidget").insert([
  { viewId: view.id, widgetCode: "my_page.section_a", sortOrder: 1, visible: true, width: "FULL", collapsed: false, useViewContext: true, config: JSON.stringify({}) },
  { viewId: view.id, widgetCode: "my_page.section_b", sortOrder: 2, visible: true, width: "HALF", collapsed: false, useViewContext: true, config: JSON.stringify({}) },
  // ... more widgets
]);

Step 4: Wrap the Frontend Page

Replace the legacy page with a feature-gated ViewRenderer:

// pages/my-page/[id]/index.tsx
import { FeatureGatedPage } from "@/components/feature-gated-page";
import { ViewRenderer } from "@/libs/shared/view-system";
import { FeatureFlag } from "@/libs/shared/feature-flags";

export default function MyPage({ id }: { id: string }) {
  const router = useRouter();

  return (
    <FeatureGatedPage
      flag={FeatureFlag.VIEW_SYSTEM}
      onFeatureDisabled={() => router.replace(`/my-page/${id}/legacy`)}
    >
      <ViewRenderer viewTypeCode="my_page" entityId={id} />
    </FeatureGatedPage>
  );
}

Step 5: Feature Flag Rollout

  1. Deploy with FeatureFlag.VIEW_SYSTEM disabled (default)
  2. Enable for internal testers in PostHog
  3. Monitor VIEW_RENDERED and VIEW_LOAD_ERROR analytics
  4. Gradually roll out to 100%
  5. Remove legacy page and feature gate once stable

Customization System

User Capabilities

Users can customize their page layouts through the view system UI:

Action How It Works
Reorder widgets Drag-and-drop — updates sortOrder on all affected ViewWidget rows
Show/hide widgets Toggle visibility — sets visible to true/false
Resize widgets Change width — sets width to any supported value (FULL, HALF, THIRD, etc.)
Collapse widgets Toggle collapsed state — sets collapsed to true/false
Configure widgets Edit widget-specific config (deep-merged with default config)

View Scopes & Permissions

Scope Who sees it Who can edit Permission check
GLOBAL Everyone (system default) CRED admins only isCredAdmin (role=ADMIN + companyId=450931)
WORKSPACE All users in a workspace Workspace admins isAdmin + matching workspaceId
PERSONAL Only the creator Owner only createdByUserId === currentUser.id
SHARED Anyone with the link Any authenticated user No restriction

Scope Permissions

All 7 view mutation use cases enforce scope-based permissions via the shared validateViewScopePermission helper. A ForbiddenError is thrown for unauthorized access. See COM-33743.

Fork-on-Edit

When a user clicks "Customize" on a GLOBAL or WORKSPACE view, the system automatically forks (copies) the view into a PERSONAL scope before entering edit mode. This ensures:

  • Regular users never modify the GLOBAL or WORKSPACE default
  • Each user gets their own customizable copy
  • The original view stays intact for all other users
┌────────────────────────────────────────────────────────────┐
│                    Fork-on-Edit Flow                        │
├────────────────────────────────────────────────────────────┤
│                                                             │
│   User clicks "Customize" on a GLOBAL view                  │
│        │                                                    │
│        ▼                                                    │
│   forkView(viewId, PERSONAL) mutation                       │
│        │                                                    │
│        ├── Creates new View (scope=PERSONAL,                │
│        │   createdByUserId=currentUser)                     │
│        ├── Copies all ViewWidget rows                       │
│        ├── Remaps parent-child widget relationships         │
│        └── Remaps cross-widget bindings                     │
│                                                             │
│   resolvedView refetch returns the PERSONAL copy            │
│        │                                                    │
│        ▼                                                    │
│   Edit mode opens on the PERSONAL copy                      │
│   (all changes saved to PERSONAL view only)                 │
│                                                             │
└────────────────────────────────────────────────────────────┘

See COM-33745 (BE) and COM-33747 (FE).

Scope Selector

Admin users see a scope selector dropdown in the customizer toolbar allowing them to choose the save target:

Scope Who can select Effect
Personal (default) Any user Changes saved to PERSONAL view for this user only
Workspace Default Workspace admins Changes saved as WORKSPACE default for all workspace members
Global Default CRED admins only Changes saved as GLOBAL default for all users
  • Non-admin users only see "Personal" as enabled
  • Selecting "Global Default" triggers a confirmation dialog: "Changes will affect all users across all workspaces"
  • The scope selector resets to PERSONAL when exiting edit mode

See COM-33748.

Scope Badge

A color-coded scope badge is shown in the view header and the edit toolbar:

Scope Color Icon
Personal Blue User
Workspace Default Amber UsersThree
Global Default Green Globe

System Widget Protection

System widgets (isSystemWidget: true on WidgetDefinition) in GLOBAL scope views are sealed:

Action GLOBAL scope PERSONAL / WORKSPACE scope
Delete Blocked (lock icon + BE validation) Allowed
Change widgetCode Blocked (BE validation) Allowed
Reorder Allowed Allowed
Resize Allowed Allowed
Show/Hide Allowed Allowed
Edit config Allowed Allowed

In the frontend, system widgets in GLOBAL edit mode show a lock icon with tooltip: "System widget — cannot be removed from the global default" instead of a delete button.

The backend enforces protection via:

  • validateSystemWidgetsOnUpdate — rejects if any system widget code is missing from the incoming widget list
  • validateSystemWidgetOnRemove — rejects removal of widgets where the definition has isSystemWidget: true

See COM-33744 (BE) and COM-33749 (FE).

Reset to Default

Users can reset their customized view back to the default:

Viewing "Reset to Default" action Falls back to
PERSONAL view Deletes the PERSONAL view WORKSPACE or GLOBAL
WORKSPACE view Deletes the WORKSPACE view (admin only) GLOBAL
GLOBAL view Button disabled ("This is already the default") N/A

Reset uses the existing deleteView mutation with scope permissions. The resolveByPriority chain automatically serves the next-highest-priority view after deletion.

Custom Dashboards

The view system supports custom dashboards via the custom_dashboard ViewTypeDefinition. Unlike entity-scoped pages (company overview, deal overview), dashboards are not inherently tied to a specific entity. They can optionally specify entity context via View.config to enable data-driven widgets.

ViewTypeDefinition:

Field Value
code custom_dashboard
entityTypeCode null (not entity-scoped by default)
routePattern /intelligence/dashboards/[id]
isSystemView true

Dashboards are listed at /intelligence/dashboards using the viewsConnection query filtered by viewTypeCode: "custom_dashboard".

See COM-33941 for the Brand Intelligence demo dashboard seed.

Entity Context Tokens (Planned)

Planned — Not Yet Implemented

This feature is tracked in COM-33943.

Dashboards can specify dynamic entity context via View.config:

{
  "entityTypeCode": "COMPANY",
  "entityId": "WORKSPACE_COMPANY_ID"
}
Token Resolves To Use Case
WORKSPACE_COMPANY_ID viewerCompanyId Dashboard scoped to workspace's company
WORKSPACE_USER_COMPANY_ID userCompanyId Tenant-scoped queries
CURRENT_USER_ID currentUser.id Dashboard personalized to the user
CURRENT_USER_PERSON_ID viewerPersonId User's person profile

When View.config.entityTypeCode is set, it overrides ViewTypeDefinition.entityTypeCode. The FE resolves tokens from the auth context before passing entity context to widgets.

Registry-Driven Config Dialogs

Each widget can register its own config dialog via the configDialog field on WidgetDefinition in the widget manifest. When a user clicks the gear icon on a widget in edit mode, the system:

  1. Looks up the widget's registered configDialog component
  2. Falls back to WidgetDataConfigDialog (generic JSON editor) if none registered
  3. Opens the dialog with the widget's current config

How to register a config dialog:

// In register-builtin-widgets.ts
{
  config: myWidgetConfig,
  loader: () => import("../widgets/my-widget"),
  configDialog: MyConfigDialog  // optional — falls back to generic editor
}

Built-in config dialogs:

Widget Dialog
generic.chart ChartConfigDialog — chart type, template, data source
generic.data_table TableConfigDialog — columns, filters, sorting
All others WidgetDataConfigDialog — generic JSON config editor

See PR #16286.

┌────────────────────────────────────────────────────────────┐
│                   View Link Lifecycle                        │
├────────────────────────────────────────────────────────────┤
│                                                             │
│   1. User creates shareable link                            │
│      └── createViewLink mutation                            │
│           └── Generates UUID token + optional expiresAt     │
│                                                             │
│   2. Link is shared externally                              │
│      └── URL: /shared-view/<token>                          │
│                                                             │
│   3. Recipient accesses the link                            │
│      ├── resolveViewLink query (public, no auth)            │
│      └── recordViewLinkAccess mutation (analytics)          │
│           └── Increments viewCount, tracks visitor hash     │
│                                                             │
│   4. Owner revokes link (optional)                          │
│      └── revokeViewLink mutation                            │
│           └── Sets isActive = false                         │
│                                                             │
└────────────────────────────────────────────────────────────┘

GraphQL API Surface

Queries

Query Description
viewTypes(entityTypeCode?) List all view type definitions, optionally filtered by entity type
widgetDefinitions(filters?) List widget definitions, optionally filtered
view(id) Get a single view by ID
resolvedView(viewTypeCode) Resolve the best view for the current user (PERSONAL → WORKSPACE → GLOBAL)
viewsConnection(filters) Paginated list of views, filterable by type and scope
viewLinks(viewId) Get all shareable links for a view
viewLinkAnalytics(viewLinkId) Get analytics summary for a view link
resolveViewLink(token) Resolve a public view link by token (no auth required)

Mutations

View management:

  • createView / updateView / deleteView
  • createViewFromTemplate — copy widgets from an existing view as a template
  • forkView(input: InputForkView!) — create a scoped copy (PERSONAL or WORKSPACE) of an existing view with all widgets

Widget management:

  • addViewWidget / removeViewWidget
  • updateViewWidget / updateViewWidgets
  • reorderViewWidgets — reorder using ordered widget IDs

View links:

  • createViewLink / revokeViewLink
  • recordViewLinkAccess — track analytics on public link access

Built-in Widget Catalog

Generic Widgets

Reusable across all entity types and view types.

Code Type Default Width Description
generic.chart CHART HALF Universal chart renderer (uses config.templateId or inline staticData)
generic.kpi_card SECTION QUARTER Single KPI metric tile — supports static values and entity metric fetching
generic.data_table TABLE_VIEW FULL Configurable data table
generic.key_takeaways SECTION FULL AI-generated key insights
generic.recent_news SECTION FULL Latest news articles
generic.similar_entities LIST_VIEW FULL Similar entity profiles
generic.top_markets SECTION FULL Key geographic markets
generic.top_entities LIST_VIEW FULL Ranked entity list
generic.recent_deals LIST_VIEW FULL Latest deals
generic.overview_metrics SECTION FULL KPI metric tiles
generic.activity_feed SECTION FULL Activity timeline

KPI Card Widget (generic.kpi_card)

The KPI card widget displays a single metric as a tile with label, value, delta indicator, and optional sparkline. It supports two modes:

Static mode — all values provided in config.data:

{
  "data": [{
    "label": "Revenue",
    "value": "$4.2M",
    "delta": "+12%",
    "deltaDir": "up",
    "subtitle": "vs last quarter"
  }]
}

Metric mode — fetches real data from entity metrics:

{
  "data": [{
    "label": "Revenue",
    "metric": { "name": "REVENUE", "valueType": "MONETARY", "outputType": "SUM" },
    "subtitle": "Current period"
  }]
}

In metric mode, the widget uses the view's entity context to fetch the metric value from the backend. Static fallback values are displayed while loading or when no entity context is available.

See COM-33942 and PR #16229.

Dashboard Widgets

For the home_dashboard view type.

Code Type Default Width
dashboard.key_metrics SECTION FULL
dashboard.recent_activity SECTION FULL
dashboard.my_lists SECTION HALF
dashboard.my_tasks SECTION HALF
dashboard.pipeline_summary SECTION FULL
dashboard.saved_reports SECTION FULL

Company Widgets

For company_overview and related company view types.

Code Type Default Width
company.about SECTION FULL
company.key_decision_makers SECTION FULL
company.sponsorship_deals SECTION FULL
company.audience_fit SECTION FULL
company.marketing_overview SECTION FULL
company.financial_summary SECTION FULL
company.offices SECTION FULL
company.audience SECTION FULL
company.teams SECTION FULL
company.sports_campaigns SECTION FULL
company.data_completeness SECTION FULL
company.apps SECTION FULL
company.lists SECTION FULL
company.competitors SECTION FULL
company.signals SECTION FULL

Person Widgets

For the person_overview view type.

Code Type Default Width
person.commonalities SECTION FULL
person.company_details SECTION FULL
person.experience SECTION FULL
person.education SECTION FULL
person.skills SECTION FULL
person.interests SECTION FULL

Deal Widgets

For the deal_overview view type.

Code Type Default Width
deal.details SECTION FULL
deal.historical_deals SECTION FULL
deal.sport_campaigns SECTION FULL
deal.audience SECTION FULL
deal.financials SECTION FULL

Region Widgets

For the region_overview view type.

Code Type Default Width
region.market_details SECTION FULL
region.news SECTION FULL

Company Market Widgets

Shared across region_overview and industry_overview view types.

Code Type Default Width
company_market.top_participants SECTION FULL
company_market.newly_public SECTION FULL
company_market.recently_funded SECTION FULL
company_market.top_streaming_spenders SECTION FULL
company_market.top_impressions SECTION FULL
company_market.recently_sponsored SECTION FULL
company_market.top_digital_spenders SECTION FULL
company_market.faster_hiring SECTION FULL

Binding System

Implemented

The binding system was delivered under COM-33636.

Container Types

The binding system will introduce container widgets that group child widgets:

Container Type Description
TABS Tabbed interface — one child visible at a time
CONTAINER Generic grouping container
SCROLL_AREA Scrollable region for overflow content

Widget Attributes & Cross-Widget Communication

Widgets will be able to declare attributes (state) and bindings (subscriptions to other widgets' attributes). This enables cross-widget communication — for example, selecting a row in a table widget could update a detail panel widget.

Key Reference

  • Linear issue: COM-33636
  • Backend architecture notes: cred-api-commercial/src/domain/view/ARCHITECTURE.md

Planned Work & Known Issues

Backlog Items

ID Title Type Status
COM-33943 View.config entity context tokens — dynamic entityTypeCode + entityId Feature Backlog
COM-33941 Seed Brand Intelligence demo dashboard as a real View record Feature Backlog
COM-33940 Refactor View table: drop userCompanyId, unify on workspaceId Refactor Backlog
COM-33927 CredFunnelChart does not render inline staticData Bug Backlog

View Table Refactor (userCompanyIdworkspaceId)

Planned Refactor

Tracked in COM-33940. Medium risk — touches core view resolution.

The View table currently has both workspaceId and userCompanyId columns that serve the same purpose (tenant scoping). The planned refactor will:

  • Backfill workspaceId from userCompanyId where null
  • Update all use cases and resolve logic to use only workspaceId
  • Drop userCompanyId column in a follow-up migration

Target scope semantics:

Scope workspaceId createdByUserId
GLOBAL NULL NULL
WORKSPACE = currentUser.companyId NULL
PERSONAL = currentUser.companyId = currentUser.id

Known Bug: Funnel Chart with Inline Data

The CredFunnelChart component does not render when fed inline staticData through the generic.chart widget pipeline. Other chart types (line, pie, column) work correctly with the same pattern. The data is filtered out somewhere in the useStableChartDatafilteredDatasortedFilteredData pipeline. See COM-33927.


Troubleshooting

Issue Check Resolution
View not loading Feature flag status, resolvedView query response Verify FeatureFlag.VIEW_SYSTEM is enabled for the user in PostHog
Widgets not rendering WidgetRegistry lookup, browser console errors Ensure widget code is registered in register-builtin-widgets.ts and the lazy import path is correct
Missing widgets on new page GLOBAL view seed data Verify the migration seeded both WidgetDefinitions and ViewWidgets for the GLOBAL view
Customizations not saving updateViewWidgets mutation response Check that a PERSONAL view was created (fork-on-edit copies it first). If saving to WORKSPACE/GLOBAL, verify user has the required scope permissions.
"Only CRED admins can modify global views" User role and company Only isCredAdmin users (ADMIN role + CRED company ID 450931) can modify GLOBAL views
Can't delete a system widget isSystemWidget on WidgetDefinition System widgets in GLOBAL views cannot be deleted (by design). Fork to PERSONAL to get a fully editable copy.
Fork-on-edit not working forkView mutation response Check that the forkView mutation succeeds. If it fails, edit mode won't open. Check browser console for GraphQL errors.
Shared link returns 404 ViewLink isActive and expiresAt Verify link is active and not expired via viewLinks query
Widget showing wrong data useViewContext flag, widget config Ensure useViewContext: true is set and the context provider passes the correct entityId
Drag-and-drop not working Frontend ViewRenderer state, sortOrder values Check for duplicate sortOrder values; reorderViewWidgets mutation normalizes them
View resolution returns wrong scope User's PERSONAL views, workspace assignment Resolution follows PERSONAL → WORKSPACE → GLOBAL; delete stale PERSONAL views to fall through

Codebase Reference

Backend (cred-api-commercial)

Path Description
src/domain/view/ARCHITECTURE.md Architecture decision records
src/domain/view/entity/ Domain entities and value objects
src/domain/view/usecase/ All view system use cases
src/domain/view/usecase/fork-view.ts ForkViewUseCase — creates scoped copies of views
src/domain/view/usecase/helpers/validate-view-scope.ts Scope permission enforcement helper
src/domain/view/usecase/helpers/validate-system-widget-protection.ts System widget protection helper
src/domain/view/usecase/helpers/copy-view-widgets.ts Shared widget copy helper (used by fork + template)
src/domain/view/repository/ Repository interfaces
src/data/models/view/ Database repository implementations
src/graphql-api/view/resolvers/ GraphQL resolvers (view, view-link, view-link-public)
src/graphql-api/view/types/ GraphQL type definitions
src/graphql-api/view/resolvers/inputs/ GraphQL input types
src/data/migrations/20260224163838_unified-view-system-tables.ts Core schema migration

Frontend (cred-web-commercial)

Path Description
libs/shared/src/view-system/ Entire view system module
libs/shared/src/view-system/types.ts Type definitions
libs/shared/src/view-system/components/view-renderer.tsx Main rendering component
libs/shared/src/view-system/components/widget-renderer.tsx Per-widget renderer
libs/shared/src/view-system/registry/widget-registry.ts Widget registry singleton
libs/shared/src/view-system/registry/register-builtin-widgets.ts Built-in widget manifest
libs/shared/src/view-system/hooks/use-resolved-view.ts View resolution hook
libs/shared/src/view-system/hooks/use-view-mutations.ts All view mutation hooks (including forkView)
libs/shared/src/view-system/components/customizer/scope-selector.tsx Scope selector dropdown
libs/shared/src/view-system/components/customizer/view-scope-badge.tsx Scope badge component
libs/shared/src/view-system/components/customizer/widget-edit-controls.tsx Widget edit controls (includes lock icon for system widgets)