Skip to content

Unified View System

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.

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

1. Architecture

Full-Stack Overview

graph TB
    subgraph Frontend ["Frontend (cred-web-commercial)"]
        FGP["FeatureGatedPage<br/>FeatureFlag.VIEW_SYSTEM"]
        VR["ViewRenderer<br/>viewTypeCode + entityId"]
        WR["WidgetRenderer<br/>per-widget error boundary"]
        REG["WidgetRegistry<br/>code → lazy component"]
        CTX["ViewContextProvider<br/>entityType + entityId"]

        FGP --> VR
        VR --> WR
        WR --> REG
        VR --> CTX
    end

    subgraph Gateway ["Apollo Federation Gateway"]
        GW["graphql_router"]
    end

    subgraph Backend ["Backend (cred-api-commercial)"]
        RES["ViewResolver<br/>GraphQL queries + mutations"]
        UC["Use Cases<br/>ResolveView, CreateView, etc."]
        REPO["Repositories<br/>View, ViewWidget, etc."]
        DB["PostgreSQL<br/>4 core tables"]

        RES --> UC
        UC --> REPO
        REPO --> DB
    end

    VR -->|"resolvedView(viewTypeCode)"| GW
    GW --> RES

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:

flowchart LR
    A["Request: viewTypeCode"] --> B{"PERSONAL view<br/>for this user?"}
    B -->|Yes| C["Return PERSONAL"]
    B -->|No| D{"WORKSPACE view<br/>for user's workspace?"}
    D -->|Yes| E["Return WORKSPACE"]
    D -->|No| F["Return GLOBAL<br/>(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.


2. Core Concepts

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)

WidgetRegistry (Frontend)

A singleton that maps widget codes to lazy-loaded React components:

// Lookup: static code → direct match
widgetRegistry.get("company.about")  // → WidgetDefinition with lazy component

// Lookup: dynamic code → prefix match + config injection
widgetRegistry.get("generic.chart")  // with config { templateId: "financial.revenue_by_segment" }

The registry supports dynamic/prefix-based widgets — a single component (e.g., generic.chart) serves multiple widget instances by varying the config.templateId.


3. Database Schema

erDiagram
    ViewTypeDefinition {
        int id PK
        string code UK
        string name
        string entityTypeCode
        string routePattern
        boolean isSystemView
    }

    WidgetDefinition {
        int id PK
        string code UK
        string name
        string widgetType
        jsonb applicableEntityTypes
        jsonb applicableViewTypes
        string defaultWidth
        jsonb defaultConfig
        string category
        boolean isSystemWidget
    }

    View {
        int id PK
        string name
        string viewTypeCode FK
        string scope
        int createdByUserId FK
        int workspaceId FK
        jsonb config
    }

    ViewWidget {
        int id PK
        int viewId FK
        string widgetCode FK
        int sortOrder
        boolean visible
        string width
        boolean collapsed
        boolean useViewContext
        jsonb config
    }

    ViewLink {
        int id PK
        int viewId FK
        string token UK
        boolean isActive
        datetime expiresAt
        int viewCount
    }

    ViewTypeDefinition ||--o{ View : "viewTypeCode"
    View ||--o{ ViewWidget : "viewId"
    WidgetDefinition ||--o{ ViewWidget : "widgetCode"
    View ||--o{ ViewLink : "viewId"

Width enum values and their grid columns:

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

4. 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.


5. 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

6. 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

Scope Who sees it Who can create it
GLOBAL Everyone (system default) Admins only
WORKSPACE All users in a workspace Workspace admins
PERSONAL Only the creator Any user
SHARED Anyone with the link Any user (via ViewLink)

When a user customizes a GLOBAL view, the system creates a PERSONAL copy. The GLOBAL view is never modified by end users.

Users can generate shareable links to any view:

  1. Create link — generates a UUID token with optional expiry date
  2. Share URL — recipients access the view via the token (no auth required)
  3. Analytics — track view count, unique visitors, last accessed
  4. Revoke — deactivate the link at any time

7. 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)
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

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

8. Future: Binding System

Planned — Not Yet Implemented

The features in this section are planned under COM-33636 and are not yet available.

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

Key Reference Files

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/repository/ Repository interfaces
src/data/models/view/ Database repository implementations
src/graphql-api/view/ GraphQL resolvers, types, inputs
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