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) |
ViewLink
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
- 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
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 listvalidateSystemWidgetOnRemove— rejects removal of widgets where the definition hasisSystemWidget: 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:
- Looks up the widget's registered
configDialogcomponent - Falls back to
WidgetDataConfigDialog(generic JSON editor) if none registered - 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.
Sharing & View Links
┌────────────────────────────────────────────────────────────┐
│ 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/deleteViewcreateViewFromTemplate— copy widgets from an existing view as a templateforkView(input: InputForkView!)— create a scoped copy (PERSONAL or WORKSPACE) of an existing view with all widgets
Widget management:
addViewWidget/removeViewWidgetupdateViewWidget/updateViewWidgetsreorderViewWidgets— reorder using ordered widget IDs
View links:
createViewLink/revokeViewLinkrecordViewLinkAccess— 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.
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 (userCompanyId → workspaceId)
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
workspaceIdfromuserCompanyIdwhere null - Update all use cases and resolve logic to use only
workspaceId - Drop
userCompanyIdcolumn 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 useStableChartData → filteredData → sortedFilteredData 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) |