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).

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;
  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, ViewConfig>;
  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[];
  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")
dbNameSnake_case SQL table name (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=
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:

interface GuardsConfig {
  createable: string[];
  updatable: string[];
  immutable: string[];
  protected: Record<string, string[]>;
}
FieldDescription
createableFields that can be set during record creation
updatableFields that can be modified on existing records
immutableFields that can be set on create but never changed
protectedFields only modifiable via named actions (field name to action names)

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;
  required: boolean;
  default?: unknown;
}
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

FieldDescription
nameField identifier
typeZod type string: "string", "number", "boolean", "array<string>"
requiredWhether the field must be provided
defaultDefault value pre-filled in the form

ViewConfig

Named column projection with access control:

interface ViewConfig {
  fields: string[];
  access: AccessRule;
  query?: {
    filterable?: string[] | null;
    searchable?: string[];
    sortable?: string[];
    defaultSort?: string;
  };
}
FieldDescription
fieldsArray of column names to include in this view
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***@acme.com
phone(555) 123-4567***-***-4567
ssn123-45-6789***-**-6789
redactAny string------

ValidationRule

Per-field validation constraints, mirrored from the Zod the API enforces on request bodies so the CMS can render the right control and validate before it POSTs:

interface ValidationRule {
  minLength?: number;
  maxLength?: number;
  min?: number;
  max?: number;
  enum?: string[];
  email?: boolean;
  url?: 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 rejects anything new URL() can't parse, including ""
maxLengthq.text({ maxLength }) / q.url({ maxLength })

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.

FieldDescription
minLengthMinimum string length
maxLengthMaximum string length
minMinimum numeric value
maxMaximum numeric value
enumArray of allowed string values
emailWhen true, validates email format

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