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? #
- Code-First Flexibility: Replace complex SQL and repetitive frontend components with typed TypeScript definitions.
- Multi-Level Cascading Filters: Build interactive dropdown cascades (e.g. Region → Country → City or Pipeline → Stage) with automatic dependency tracking and dynamic option resolution.
- Primary Key & Row Actions: Designate primary key columns (
isPrimaryKey: true) for deterministic row identity, multi-row checkbox selection, and contextual row actions. - Bulk & Single-Row Actions: Attach typed server-side actions triggered from table row dropdowns or the floating bulk action bar.
- System-Managed Pagination: Enjoy built-in page and limit controls, state persistence, and responsive viewport sizing without writing boilerplate table pagination.
- 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:
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:
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:
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:
// 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:
┌─────────────────────────────────────────────────────────────────┐
│ 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:
| Constraint | Limit | Behavior on Violation |
|---|---|---|
| Data & Action Timeout | 10,000 ms (10 seconds) | Aborts isolate; returns execution timeout error. |
| Dynamic Options Timeout | 5,000 ms (5 seconds) | Aborts evaluation; returns dynamic options timeout. |
| Isolate Memory Cap | 64 MB (32 MB for options) | Immediately disposes isolate if heap allocation is exceeded. |
| CRM Call Budget | 50 operations per invocation | Throws Exceeded maximum CRM calls per execution (50). |
| HTTP Call Budget | 5 requests per invocation | Throws Exceeded maximum HTTP calls per execution (5). |
| HTTP Request Timeout | 8,000 ms max per request | Aborts request via AbortController signal. |
| HTTP Response Payload | 1 MB (1,048,576 bytes) | Throws Response size exceeds maximum limit. |
| SSRF Filter | Prohibits private subnets & localhost | Throws SSRF Blocked: URL resolves to forbidden range. |
Live IDE Workflow #
- Create or Open Page: Navigate to Custom Functions & Listings IDE and select or create a Custom Listing Page.
- File Tree Navigation: Browse
filters.ts,fetcher.ts, and youractionsfolder in the sidebar. - Auto-Typed SDK: Monaco automatically generates concrete TypeScript types (
__ListingRow,__ListingFilters) matching yourcolumnsandfiltersdefinitions. - Compile (
Ctrl+S): Saving any file bundles and compiles the project. - Instant Preview: Click View Page in the IDE toolbar to open
/listing-pages/[slug]in a live viewport.
Next Steps #
- Filters & Columns Configuration — Filter types, static/dynamic options, cascading dependencies, and column schemas.
- Fetcher & Pagination — Authoring the data fetcher, CRM queries, and system pagination.
- Custom Actions & Row Operations — Guide to authoring single-row and bulk actions with toast feedback and table reloading.
- Encrypted Secrets & Keys — Managing securely encrypted API keys and third-party credentials.