Quickback Docs

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[]>;
}
FieldDescription
generatedByAlways "quickback-compiler"
versionCompiler version string
featuresMap of feature name to array of source file names
tablesMap of camelCase table name to full table metadata
tablesByFeatureMap of feature name to array of table names in that feature
featureActionsMap of feature name to standalone actions not attached to one table
pagesMap of page slug to full page metadata
pagesByFeatureMap 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
}
FieldDescription
namecamelCase table name (e.g., "accountCode") — the key in tables, and in the client's resources
dbNameSnake_case SQL table name (e.g., "account_code")
apiPathKebab-case URL segment (e.g., "account-code")
featureParent feature name (e.g., "accounting")
columnsOrdered array of column metadata
firewallTenant isolation config (organization, owner, softDelete, exception)
crudLegacy normalized write config kept for backwards compatibility
create / update / delete / upsertFlat write-operation aliases mirroring the authored defineTable(...) DSL
guardsField-level create/update/immutable/protected rules
maskingPer-field masking rules keyed by column name
viewsNamed column projections keyed by view name
defaultViewAuthored read.defaultView, used for initial CMS and FK lookup reads
requiresExplicitViewEmitted as true when named views exist without a default and the CMS must not issue a bare collection read
validationPer-field validation rules keyed by column name
actionsArray of action definitions scoped to this table
displayColumnColumn used as human-readable label (auto-detected or explicit)
defaultSortDefault sort for CMS table list view (e.g., { field: "createdAt", order: "desc" })
inputHintsMap of column name to preferred CMS input type (e.g., "richtext", "select", "textarea", "checkbox")
layoutsNamed record page layouts keyed by layout name (see CmsLayout below)
internalWhen true, table is hidden from CMS sidebar
ownsRelationsThe owns config verbatim — the CMS renders these as inline owned-records sections on the root's record page
ownedBySet on owned children: the aggregate-root table that owns this table
readIncludeThe read.include allowlist — tokens fetchable via ?include=
emitsAfterCommitThe aggregate root emits one CloudEvent after each changeset commit
routesDisabledOps 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;
}
FieldDescription
namecamelCase property name
dbNameSnake_case SQL column name
typeSQLite storage type
modeWhen "boolean", an integer column represents true/false
kindq-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.
primaryKeyWhether this column is the primary key
notNullWhether the column has a NOT NULL constraint
defaultValueStatic default value (strings, numbers, or booleans)
fkTargetTarget 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>;
}
FieldDescription
access.rolesArray of roles allowed for this operation
access.orAlternative access conditions (any must match)
access.recordRecord-level conditions for access
modeOperation mode (e.g., "batch" for bulk create)
defaults / computedCreate-time defaults and computed field metadata
maxBatchSize / allowFailFastBatch-write controls when the config describes a batch operation
batchNested 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[]>;
}
FieldDescription
createableFields 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.
updatableFields PATCH accepts. With guards: updatable. Without: every column except the primary key. Audit, firewall/scope, immutable, and protected fields are never included.
immutableThe declared guards.immutable list: set on create, never changed.
protectedThe 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;
}
FieldDescription
nameAction identifier (e.g., "approve", "applyPayment")
descriptionHuman-readable description shown in dialog
inputFieldsArray of input field definitions
access.rolesRoles allowed to execute this action
access.recordRecord conditions (e.g., { status: { equals: "pending" } })
standaloneWhen true, action is not tied to a specific record
pathCustom API path (overrides default)
methodHTTP method (defaults to POST)
responseType"file" for download responses
sideEffects"sync" for actions with synchronous side effects
cmsOptional 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 in tables.

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.

FieldDescription
nameInput property name
type"string", "number", "boolean", "enum", "object", "array<item>" (item type inside the brackets) or "unknown"
requiredListed in the schema's required and has no default
defaultThe schema default (.default(x)), pre-filled in the form
enumAllowed values — z.enum, z.literal, or a union of literals
integertrue for z.number().int() (type stays "number")
nullabletrue when the field accepts null
descriptionThe 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;
}
FieldDescription
fieldsThe 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
aggregateOnlytrue when the view returns only aggregations, no rows
accessEffective role-based access: the view override or inherited table read.access
queryEffective 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;
}
FieldDescription
sectionsOrdered array of field sections

CmsLayoutSection

FieldDescription
labelSection header text
fieldsArray of column names to display in this section
columns1 (default) or 2 for two-column field layout
collapsedWhen 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;
  };
}
FieldDescription
typeMasking pattern to apply
show.rolesRoles that see the unmasked value
show.orAlternative condition for showing unmasked value

Masking Patterns

TypeInputOutput
emailjohn@acme.comj***@a***.com
phone555-123-4567******4567
ssn123-45-6789*****6789
redactAnything[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;
}
RuleEmitted from
enumq.enum([...]), or text(..., { enum: [...] }) in raw Drizzle — the CMS renders a select
urlq.url() — the CMS renders a url input; the API accepts only absolute http(s) URLs (so not "", javascript: or data:)
maxLengthq.text({ maxLength }) / q.url({ maxLength })
minLengthq.text({ minLength })
emailq.text({ email: true }) — the CMS renders an email input
integerq.int(), q.bigint(), q.smallint() — whole numbers only
min / maxq.bigint() (±2^53−1, the JSON-safe range) and q.smallint() (−32768…32767), inclusive
patternq.text({ pattern: /re/ }) (regex source only; flags are not carried) and q.decimal({ precision, scale }) — the regex source the value must match
minLength / emailOnly 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>;
}
FieldDescription
slugURL-safe page identifier
titleDisplay title
descriptionHuman-readable description
iconIcon name for sidebar
featureParent feature name
accessRole-based access control
dataSourcesNamed data source bindings
layoutLayout configuration
matchingMatching/reconciliation rules
pageActionsActions 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;
  };
}
FieldDescription
enabledWhether matching is active
rulesArray of matching rules with weights (0-1)
confidenceThresholdMinimum score to display a match suggestion (0-1, default 0.5)
condition.leftLeft field reference (dataSourceName.fieldName)
condition.rightRight field reference
condition.operatorComparison operator
condition.valueOperator 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;
}
FieldDescription
tableTarget table for the action
actionAction name on that table
inputMappingMaps action input fields to data source field references ("dataSourceName.$fieldName")
labelButton label (defaults to action name)
iconButton icon name
confirmConfirmation prompt shown before execution

Next Steps

On this page