Custom Listing Pages — Overview & Architecture #

Custom Listing Pages empower developers to create reactive, code-first data views embedded directly into the Dolfin workspace. Rather than relying on rigid predefined views, you define custom filter controls, declare typed table columns, write sandboxed server-side fetcher functions, and attach custom single-row and bulk actions.


Why Custom Listing Pages? #

  1. Code-First Flexibility: Replace complex SQL and repetitive frontend components with typed TypeScript definitions.
  2. Multi-Level Cascading Filters: Build interactive dropdown cascades (e.g. Region → Country → City or Pipeline → Stage) with automatic dependency tracking and dynamic option resolution.
  3. Primary Key & Row Actions: Designate primary key columns (isPrimaryKey: true) for deterministic row identity, multi-row checkbox selection, and contextual row actions.
  4. Bulk & Single-Row Actions: Attach typed server-side actions triggered from table row dropdowns or the floating bulk action bar.
  5. System-Managed Pagination: Enjoy built-in page and limit controls, state persistence, and responsive viewport sizing without writing boilerplate table pagination.
  6. Tenant-Safe V8 Sandboxing: All data fetching, actions, and dynamic options execute in memory-capped, time-bounded V8 isolates with strict SSRF protection and call budgets.

Multi-File Architecture #

Every custom listing page consists of core configuration files and optional custom action files authored in the Custom Functions IDE:

typescript
listing-page/
├── filters.ts      # Declarative filter definitions & typed table columns schema
├── fetcher.ts      # Sandboxed async data resolver returning items and total
└── actions/        # Optional custom single-row and bulk actions
    ├── qualify.ts  # Single-row action (type: "row", receives context.row)
    └── sync-all.ts # Bulk action (type: "rows", receives context.rows)

1. filters.ts — Interactive Filters & Column Schema #

filters.ts exports filters (toolbar controls) and columns (typed table columns). You can designate a primary key column using isPrimaryKey: true:

typescript
export const filters = defineFilters([
  {
    key: "status",
    label: "Lead Status",
    type: "select",
    options: [
      { label: "New", value: "new" },
      { label: "Contacted", value: "contacted" },
      { label: "Qualified", value: "qualified" },
      { label: "Converted", value: "converted" },
    ],
    defaultValue: "new",
  },
  {
    key: "search",
    label: "Search Leads",
    type: "text",
    placeholder: "Search by name or email...",
  },
]);

export const columns = defineColumns([
  { key: "id", label: "Lead ID", type: "number", width: 80, isPrimaryKey: true },
  { key: "name", label: "Full Name", type: "text" },
  { key: "email", label: "Email Address", type: "text" },
  { key: "status", label: "Status", type: "badge" },
  { key: "createdAt", label: "Created At", type: "datetime" },
]);

export default { filters, columns };

2. fetcher.ts — Server-Side Data Resolver #

fetcher.ts exports an asynchronous run function. Whenever the user alters a filter control or navigates pages, the backend invokes run inside an isolated V8 sandbox, injecting sanitized filter values, pagination parameters, and the scoped SDK:

typescript
export async function run(
  context: CustomListContext,
  dolfin: DolfinSdk
): Promise<CustomListResponse> {
  const { filter, pagination, moduleId } = context;
  const { page, limit } = pagination;

  // Obtain module-scoped CRM API bridge
  const crm = await dolfin.crm.module(moduleId);

  // Query CRM leads matching active filter criteria
  const leadsResult = await crm.leads.list({
    page,
    limit,
    status: filter.status ? [filter.status] : undefined,
    query: filter.search || undefined,
  });

  return {
    items: leadsResult.data || [],
    total: leadsResult.total || 0,
  };
}

3. actions/*.ts — Custom Row & Bulk Actions #

Action files allow users to trigger server-side logic from the listing view. For example, a single-row action:

typescript
// actions/qualify.ts
export async function run(
  context: CustomListRowActionContext,
  dolfin: DolfinSdk
): Promise<CustomListActionResponse> {
  const crm = await dolfin.crm.module(context.moduleId);
  await crm.leads.update(Number(context.row.id), { meta: { status: "qualified" } });

  return {
    success: true,
    message: `Lead ${context.row.name} qualified!`,
    refresh: true, // Automatically reloads listing table data
  };
}

Execution Lifecycle #

The following diagram illustrates how user interactions trigger dynamic filter updates, sandboxed data retrieval, and custom action executions:

typescript
┌─────────────────────────────────────────────────────────────────┐
│                        User Interface                           │
│  • Interactive Filter Controls (rendered from filters.ts)       │
│  • Dynamic Data Table & Checkboxes (using isPrimaryKey column)  │
│  • Row Action Menu & Floating Bulk Action Bar                   │
└────────────────────────────────┬────────────────────────────────┘
                                 │
     User Action / Filter Change │ (sanitized JSON payload)
                                 ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Host API Gateway                           │
│  • Validates filter keys and types against filtersSchema        │
│  • Sanitizes pagination: page >= 1, limit between 1 and 100     │
│  • Injects caller tenantId, moduleId, and decrypted secrets     │
└────────────────────────────────┬────────────────────────────────┘
                                 │
                     Spawns V8 Isolate (64 MB cap)
                                 ▼
┌─────────────────────────────────────────────────────────────────┐
│                    Isolated V8 Sandbox                          │
│                                                                 │
│   Fetcher: run(context: CustomListContext, dolfin)              │
│   Action:  run(context: CustomListActionContext, dolfin)        │
│                                                                 │
│   ├── context.row / context.rows: Strongly typed row data       │
│   ├── context.filter: Active filter values dictionary           │
│   ├── context.pagination: { page, limit, offset }               │
│   ├── dolfin.crm.module(id): Scoped CRM database bridge         │
│   └── dolfin.http: Outbound HTTP client with SSRF filter        │
└────────────────────────────────┬────────────────────────────────┘
                                 │
               Returns: { items, total } or Action Response
                                 ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Dynamic Listing Table                       │
│  • Renders rows with custom cell formatting                     │
│  • Shows toast notifications (success / error messages)         │
│  • Triggers table data refetch if action requested refresh      │
└─────────────────────────────────────────────────────────────────┘

Sandbox Limits & Safety Budgets #

To ensure high availability and prevent resource exhaustion across tenant workloads, custom listing executions are strictly governed by the following safety boundaries:

ConstraintLimitBehavior on Violation
Data & Action Timeout10,000 ms (10 seconds)Aborts isolate; returns execution timeout error.
Dynamic Options Timeout5,000 ms (5 seconds)Aborts evaluation; returns dynamic options timeout.
Isolate Memory Cap64 MB (32 MB for options)Immediately disposes isolate if heap allocation is exceeded.
CRM Call Budget50 operations per invocationThrows Exceeded maximum CRM calls per execution (50).
HTTP Call Budget5 requests per invocationThrows Exceeded maximum HTTP calls per execution (5).
HTTP Request Timeout8,000 ms max per requestAborts request via AbortController signal.
HTTP Response Payload1 MB (1,048,576 bytes)Throws Response size exceeds maximum limit.
SSRF FilterProhibits private subnets & localhostThrows SSRF Blocked: URL resolves to forbidden range.

Live IDE Workflow #

  1. Create or Open Page: Navigate to Custom Functions & Listings IDE and select or create a Custom Listing Page.
  2. File Tree Navigation: Browse filters.ts, fetcher.ts, and your actions folder in the sidebar.
  3. Auto-Typed SDK: Monaco automatically generates concrete TypeScript types (__ListingRow, __ListingFilters) matching your columns and filters definitions.
  4. Compile (Ctrl+S): Saving any file bundles and compiles the project.
  5. Instant Preview: Click View Page in the IDE toolbar to open /listing-pages/[slug] in a live viewport.

Next Steps #