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
- Deploy with
FeatureFlag.VIEW_SYSTEMdisabled (default) - Enable for internal testers in PostHog
- Monitor
VIEW_RENDEREDandVIEW_LOAD_ERRORanalytics - Gradually roll out to 100%
- 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.
Sharing & View Links
Users can generate shareable links to any view:
- Create link — generates a UUID token with optional expiry date
- Share URL — recipients access the view via the token (no auth required)
- Analytics — track view count, unique visitors, last accessed
- 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 |