Schema Format Reference
Complete TypeScript type definitions for the schema-registry.json format.
Schema Format Reference
The schema registry is a JSON file with a well-defined structure. Below are the complete TypeScript type definitions used by both the compiler (to generate) and the CMS (to consume).
The compiler's own definitions are emitted verbatim into
quickback.client.ts as SchemaRegistry,
TableMeta, ViewMeta, ValidationRule, ActionInputField and friends —
import them from there rather than re-declaring them. Where the emitted type is
looser than the one shown here (Record<string, unknown> for crud,
masking, firewall), the shape below is what the CMS relies on.
SchemaRegistry
The top-level type:
interface SchemaRegistry {
generatedBy: string;
version: string;
features: Record<string, string[]>;
tables: Record<string, TableMeta>;
tablesByFeature: Record<string, string[]>;
featureActions: Record<string, ActionMeta[]>;
pages: Record<string, PageMeta>;
pagesByFeature: Record<string, string[]>;
}| Field | Description |
|---|---|
generatedBy | Always "quickback-compiler" |
version | Compiler version string |
features | Map of feature name to array of source file names |
tables | Map of camelCase table name to full table metadata |
tablesByFeature | Map of feature name to array of table names in that feature |
featureActions | Map of feature name to standalone actions not attached to one table |
pages | Map of page slug to full page metadata |
pagesByFeature | Map of feature name to array of page slugs in that feature |
The registry deliberately omits wall-clock generation metadata. Every field is
derived from compiler inputs, so identical logical input produces
byte-identical schema-registry.json output across consecutive compiles.
TableMeta
Full metadata for a single table:
interface TableMeta {
name: string;
dbName: string;
apiPath: string;
feature: string;
columns: ColumnMeta[];
firewall: Record<string, unknown>;
crud: Record<string, CrudConfig>;
create?: WriteOperationConfig | false;
update?: WriteOperationConfig | false;
delete?: WriteOperationConfig | false;
upsert?: WriteOperationConfig | false;
guards: GuardsConfig;
masking: Record<string, MaskingRule>;
views: Record<string, ViewMeta>;
defaultView?: string;
requiresExplicitView?: true;
validation: Record<string, ValidationRule>;
actions: ActionMeta[];
displayColumn?: string;
defaultSort?: { field: string; order: 'asc' | 'desc' };
inputHints?: Record<string, string>;
layouts?: Record<string, CmsLayout>;
internal?: boolean;
ownsRelations?: Record<string, OwnsRelation>;
ownedBy?: string;
readInclude?: string[];
emitsAfterCommit?: boolean;
routesDisabled?: string[];
}
interface OwnsRelation {
table: string; // owned child table (camelCase registry name)
fk: string; // FK on the child pointing at this root — compiler-stamped
inherit?: string[]; // parent columns copied onto inserted children — compiler-stamped
refs?: Record<string, string>; // client-supplied pointers at independent entities
owns?: Record<string, OwnsRelation>; // nested owned relations
}| Field | Description |
|---|---|
name | camelCase table name (e.g., "accountCode") — the key in tables, and in the client's resources |
dbName | Snake_case SQL table name (e.g., "account_code") |
apiPath | Kebab-case URL segment (e.g., "account-code") |
feature | Parent feature name (e.g., "accounting") |
columns | Ordered array of column metadata |
firewall | Tenant isolation config (organization, owner, softDelete, exception) |
crud | Legacy normalized write config kept for backwards compatibility |
create / update / delete / upsert | Flat write-operation aliases mirroring the authored defineTable(...) DSL |
guards | Field-level create/update/immutable/protected rules |
masking | Per-field masking rules keyed by column name |
views | Named column projections keyed by view name |
defaultView | Authored read.defaultView, used for initial CMS and FK lookup reads |
requiresExplicitView | Emitted as true when named views exist without a default and the CMS must not issue a bare collection read |
validation | Per-field validation rules keyed by column name |
actions | Array of action definitions scoped to this table |
displayColumn | Column used as human-readable label (auto-detected or explicit) |
defaultSort | Default sort for CMS table list view (e.g., { field: "createdAt", order: "desc" }) |
inputHints | Map of column name to preferred CMS input type (e.g., "richtext", "select", "textarea", "checkbox") |
layouts | Named record page layouts keyed by layout name (see CmsLayout below) |
internal | When true, table is hidden from CMS sidebar |
ownsRelations | The owns config verbatim — the CMS renders these as inline owned-records sections on the root's record page |
ownedBy | Set on owned children: the aggregate-root table that owns this table |
readInclude | The read.include allowlist — tokens fetchable via ?include= |
emitsAfterCommit | The aggregate root emits one CloudEvent after each changeset commit |
routesDisabled | Ops declared routes: false — access exists (changeset admission) but no raw HTTP route, so the CMS suppresses New/Edit/Delete buttons and inline editing for them |
ColumnMeta
Metadata for a single column:
interface ColumnMeta {
name: string;
dbName: string;
type: "text" | "integer" | "real" | "blob";
mode?: "boolean";
kind?: string;
primaryKey: boolean;
notNull: boolean;
defaultValue?: string | number | boolean;
fkTarget?: string;
}| Field | Description |
|---|---|
name | camelCase property name |
dbName | Snake_case SQL column name |
type | SQLite storage type |
mode | When "boolean", an integer column represents true/false |
kind | q-DSL column kind (text, url, int, bool, uuid, timestamp, json, id, enum, scope, stamp) — q-authored columns only. The CMS picks the form control from it (timestamp → date-time picker, bool → checkbox…), so inputHints is only needed to override. Absent on raw-Drizzle tables, where type is all there is. |
primaryKey | Whether this column is the primary key |
notNull | Whether the column has a NOT NULL constraint |
defaultValue | Static default value (strings, numbers, or booleans) |
fkTarget | Target table name for FK columns (e.g., "contact" for a vendorId column) |
CRUDConfig
Per-operation access control:
interface CrudConfig {
access?: AccessRule;
mode?: string;
}
interface WriteOperationConfig extends CrudConfig {
defaults?: Record<string, unknown>;
computed?: Record<string, unknown>;
fields?: string[];
maxBatchSize?: number;
allowFailFast?: boolean;
batch?: false | Record<string, unknown>;
}
interface AccessRule {
roles?: string[];
or?: Array<{
roles?: string[];
record?: Record<string, unknown>;
}>;
record?: Record<string, unknown>;
}| Field | Description |
|---|---|
access.roles | Array of roles allowed for this operation |
access.or | Alternative access conditions (any must match) |
access.record | Record-level conditions for access |
mode | Operation mode (e.g., "batch" for bulk create) |
defaults / computed | Create-time defaults and computed field metadata |
maxBatchSize / allowFailFast | Batch-write controls when the config describes a batch operation |
batch | Nested batch config for the flat create / update / delete / upsert aliases |
GuardsConfig
Field-level control for create and update operations. createable and
updatable are the resolved write policy, not a copy of the table's
guards block: the compiler emits exactly the fields the generated POST /
PATCH routes accept — the same set the OpenAPI <Table>Create /
<Table>Update request bodies document. They are populated for every
resourceful table, including tables that declare no guards.
interface GuardsConfig {
createable: string[];
updatable: string[];
immutable: string[];
protected: Record<string, string[]>;
}| Field | Description |
|---|---|
createable | Fields POST accepts. With guards: createable plus immutable. Without: every column except the primary key. Both then drop audit columns, firewall/scope columns, and protected fields. Under generateId: false the client-supplied primary key is included; the optional id from allowClientIds is not. |
updatable | Fields PATCH accepts. With guards: updatable. Without: every column except the primary key. Audit, firewall/scope, immutable, and protected fields are never included. |
immutable | The declared guards.immutable list: set on create, never changed. |
protected | The declared guards.protected map: fields only modifiable via the named actions. |
Columns with a default are included, since the route accepts them as optional.
Order follows guards when declared, otherwise column order.
ActionMeta
Metadata for a custom action:
interface ActionMeta {
name: string;
description: string;
inputFields: ActionInputField[];
access?: {
roles: string[];
record?: Record<string, unknown>;
};
transition?: ActionTransitionMeta; // record actions with a state machine
standalone?: boolean;
path?: string;
method?: string;
responseType?: string;
sideEffects?: string;
cms?: CmsConfig;
}
/** The state-machine subset of the action's `transition` policy. Write-side
* keys (stamp, clears, onIllegal) are omitted; a `custom` guard function is
* replaced by `hasCustomGuard: true`. The CMS hides the action when the
* record's current state cannot take it — see /ui/admin/actions#transition-state. */
interface ActionTransitionMeta {
field?: string | null;
fromTo?: Record<string, Array<string | number | boolean>>;
to?: string | number | boolean;
via?: string;
guard?: Record<string, string | number | boolean | { null: true } | { notNull: true }>;
hasCustomGuard?: boolean;
idempotent?: "noop" | "error";
}
interface CmsConfig {
label?: string;
icon?: string;
confirm?: string | boolean;
destructive?: boolean;
category?: string;
hidden?: boolean;
placement?: 'feature' | 'tables';
tables?: string[];
successMessage?: string;
onSuccess?: 'refresh' | 'redirect:list' | 'close';
order?: number;
}
interface ActionInputField {
name: string;
type: string; // "string" | "number" | "boolean" | "enum" | "object" | "array<item>" | "unknown"
required: boolean;
default?: unknown;
enum?: Array<string | number | boolean>;
integer?: true;
nullable?: true;
description?: string;
}| Field | Description |
|---|---|
name | Action identifier (e.g., "approve", "applyPayment") |
description | Human-readable description shown in dialog |
inputFields | Array of input field definitions |
access.roles | Roles allowed to execute this action |
access.record | Record conditions (e.g., { status: { equals: "pending" } }) |
standalone | When true, action is not tied to a specific record |
path | Custom API path (overrides default) |
method | HTTP method (defaults to POST) |
responseType | "file" for download responses |
sideEffects | "sync" for actions with synchronous side effects |
cms | Optional CMS rendering metadata (label, icon, confirm, destructive, category, hidden, placement, tables, successMessage, onSuccess, order) |
CmsConfig placement rules
placement is only relevant for standalone actions in multi-table features:
"feature"shows the action in the toolbar for every table in the feature."tables"shows the action only for the tables listed intables.
If the action is already unambiguously table-scoped, the compiler can infer placement and placement is optional.
ActionInputField
One entry per top-level property of the action's input, derived from the
action's input JSON Schema — the same harvested z.toJSONSchema() output
that feeds OpenAPI and Api.<x>.actions.<name>.Input (a static parse of the
Zod source is the fallback when the harvest is unavailable). Enum values,
defaults and .describe() text survive.
| Field | Description |
|---|---|
name | Input property name |
type | "string", "number", "boolean", "enum", "object", "array<item>" (item type inside the brackets) or "unknown" |
required | Listed in the schema's required and has no default |
default | The schema default (.default(x)), pre-filled in the form |
enum | Allowed values — z.enum, z.literal, or a union of literals |
integer | true for z.number().int() (type stays "number") |
nullable | true when the field accepts null |
description | The field's .describe() text |
ViewMeta
Named column projection with access control:
interface ViewMeta {
fields: string[];
access: AccessRule;
query: {
filterable: string[] | null;
searchable: string[];
sortable?: string[];
defaultSort?: string;
};
aggregateOnly?: true;
}| Field | Description |
|---|---|
fields | The columns every row of this view carries, in order — always resolved (the view's projection, else every column; [] for an aggregate-only view), so a UI can lay out the view's columns before the first row arrives |
aggregateOnly | true when the view returns only aggregations, no rows |
access | Effective role-based access: the view override or inherited table read.access |
query | Effective view query metadata. Explicit named-view allowlists are authoritative. Current registries materialize inherited searchable and q-table filterable defaults; searchable: [] disables search, while filterable: null preserves raw Drizzle compatibility when no closed allowlist can be derived. |
CmsLayout
Named record page layout with ordered sections:
interface CmsLayout {
sections: CmsLayoutSection[];
}
interface CmsLayoutSection {
label: string;
fields: string[];
columns?: 1 | 2;
collapsed?: boolean;
}| Field | Description |
|---|---|
sections | Ordered array of field sections |
CmsLayoutSection
| Field | Description |
|---|---|
label | Section header text |
fields | Array of column names to display in this section |
columns | 1 (default) or 2 for two-column field layout |
collapsed | When true, section starts collapsed with a toggle to expand |
Fields not assigned to any section in the active layout are collected into an "Other Fields" section.
MaskingRule
Per-field data masking:
interface MaskingRule {
type: "email" | "phone" | "ssn" | "redact";
show: {
roles: string[];
or?: string;
};
}| Field | Description |
|---|---|
type | Masking pattern to apply |
show.roles | Roles that see the unmasked value |
show.or | Alternative condition for showing unmasked value |
Masking Patterns
| Type | Input | Output |
|---|---|---|
email | john@acme.com | j***@a***.com |
phone | 555-123-4567 | ******4567 |
ssn | 123-45-6789 | *****6789 |
redact | Anything | [REDACTED] |
A redacted non-string column (number, boolean, JSON, array) arrives as the
string "[REDACTED]" — see Masking.
ValidationRule
Per-field validation constraints, read back from the Zod the API enforces on request bodies — the registry never advertises a rule the server doesn't check. The CMS uses them to render the right control and validate before it POSTs:
interface ValidationRule {
enum?: string[];
maxLength?: number;
minLength?: number;
min?: number;
max?: number;
integer?: boolean;
pattern?: string;
url?: boolean;
email?: boolean;
}| Rule | Emitted from |
|---|---|
enum | q.enum([...]), or text(..., { enum: [...] }) in raw Drizzle — the CMS renders a select |
url | q.url() — the CMS renders a url input; the API accepts only absolute http(s) URLs (so not "", javascript: or data:) |
maxLength | q.text({ maxLength }) / q.url({ maxLength }) |
minLength | q.text({ minLength }) |
email | q.text({ email: true }) — the CMS renders an email input |
integer | q.int(), q.bigint(), q.smallint() — whole numbers only |
min / max | q.bigint() (±2^53−1, the JSON-safe range) and q.smallint() (−32768…32767), inclusive |
pattern | q.text({ pattern: /re/ }) (regex source only; flags are not carried) and q.decimal({ precision, scale }) — the regex source the value must match |
minLength / email | Only when the enforced Zod carries .min(n) / .email(); no q column kind emits them today |
Columns with no constraints have no entry. Optional fields left blank in the CMS form are omitted from the request body rather than sent as "" — the API accepts an optional field being absent, not empty.
PageMeta
Full metadata for a custom page:
interface PageMeta {
slug: string;
title: string;
description?: string;
icon?: string;
feature: string;
access?: { roles: string[] };
dataSources: Record<string, PageDataSource>;
layout: PageLayout;
matching?: PageMatching;
pageActions?: Record<string, PageAction>;
}| Field | Description |
|---|---|
slug | URL-safe page identifier |
title | Display title |
description | Human-readable description |
icon | Icon name for sidebar |
feature | Parent feature name |
access | Role-based access control |
dataSources | Named data source bindings |
layout | Layout configuration |
matching | Matching/reconciliation rules |
pageActions | Actions triggered from the page |
PageDataSource
interface PageDataSource {
table: string;
defaultFilters?: Record<string, unknown>;
defaultSort?: { field: string; order: 'asc' | 'desc' };
displayColumns?: string[];
}PageLayout
interface PageLayout {
type: 'split-panel';
panels: PagePanel[];
}
interface PagePanel {
id: string;
title: string;
dataSource: string;
position: 'left' | 'right';
features?: string[];
}Panel features can include "drag-source" and "drop-target" for drag-and-drop interactions.
PageMatching
interface PageMatching {
enabled: boolean;
rules: MatchingRule[];
confidenceThreshold?: number;
}
interface MatchingRule {
name: string;
weight: number;
condition: {
left: string;
right: string;
operator: 'abs-equals' | 'within-days' | 'fuzzy-match';
value?: number;
};
}| Field | Description |
|---|---|
enabled | Whether matching is active |
rules | Array of matching rules with weights (0-1) |
confidenceThreshold | Minimum score to display a match suggestion (0-1, default 0.5) |
condition.left | Left field reference (dataSourceName.fieldName) |
condition.right | Right field reference |
condition.operator | Comparison operator |
condition.value | Operator parameter (e.g., max days for within-days) |
PageAction
interface PageAction {
table: string;
action: string;
inputMapping: Record<string, string>;
label?: string;
icon?: string;
confirm?: string;
}| Field | Description |
|---|---|
table | Target table for the action |
action | Action name on that table |
inputMapping | Maps action input fields to data source field references ("dataSourceName.$fieldName") |
label | Button label (defaults to action name) |
icon | Button icon name |
confirm | Confirmation prompt shown before execution |
Next Steps
- Schema Registry — How the registry is generated and used
- Components Reference — All CMS React components