Quickback Docs

Schema Registry

The JSON metadata file generated by the Quickback compiler that powers the CMS.

Schema Registry

The schema registry is a static JSON file generated by the Quickback compiler. It contains full metadata about every table in your project — columns, types, guards, masking rules, views, actions, validation, and firewall config. The CMS reads this file to render its entire UI.

Generation

Schema registry generation is enabled by default. Run quickback build and the compiler outputs schema-registry.json alongside your compiled API files.

To disable generation:

quickback/quickback.config.ts
export default defineConfig({
  schemaRegistry: { generate: false },
  // ... rest of your config
});

Output Shape

The top-level structure of schema-registry.json:

{
  "generatedBy": "quickback-compiler",
  "version": "1.0.0",
  "features": { ... },
  "tables": { ... },
  "tablesByFeature": { ... },
  "featureActions": { ... },
  "pages": { ... },
  "pagesByFeature": { ... }
}
FieldTypeDescription
generatedBystringAlways "quickback-compiler"
versionstringCompiler version used
featuresRecord<string, string[]>Feature name to file list mapping
tablesRecord<string, TableMeta>Table name to full metadata
tablesByFeatureRecord<string, string[]>Feature name to table name list
featureActionsRecord<string, ActionMeta[]>Feature name to standalone actions not owned by one table
pagesRecord<string, PageMeta>Page slug to full page metadata
pagesByFeatureRecord<string, string[]>Feature name to page slug list

The registry contains no wall-clock generation timestamp. Its bytes are fully derived from the logical compiler input, so repeated compiles are stable for artifact hashing, caches, and deployment diffs.

TableMeta

Each table entry contains everything the CMS needs to render its UI:

interface TableMeta {
  name: string;              // camelCase table name (e.g., "accountCode")
  dbName: string;            // SQL table name (e.g., "account_code")
  feature: string;           // Parent feature name
  columns: ColumnMeta[];     // All columns including audit fields
  firewall: Record<string, unknown>;  // Tenant isolation config
  crud: Record<string, CrudConfig>;   // Legacy normalized write surface
  create?: WriteOperationConfig | false;  // Flat create config
  update?: WriteOperationConfig | false;  // Flat update config
  delete?: WriteOperationConfig | false;  // Flat delete config
  upsert?: WriteOperationConfig | false;  // Flat upsert config
  guards: {
    createable: string[];    // Fields allowed on create
    updatable: string[];     // Fields allowed on update
    immutable: string[];     // Fields locked after creation
    protected: Record<string, string[]>;  // Fields writable only via named actions/transitions
  };
  masking: Record<string, MaskingRule>;  // Per-field masking rules
  views: Record<string, ViewConfig>;     // Named column projections
  defaultView?: string;       // Authored read.defaultView
  requiresExplicitView?: true; // Named views exist with no default
  validation: Record<string, ValidationRule>;  // Per-field validation
  actions: ActionMeta[];     // Table-scoped actions for this table
  displayColumn?: string;    // Human-readable label column
  internal?: boolean;        // Hidden from CMS sidebar when true
}

Each named view carries the effective read contract the CMS consumes:

interface ViewConfig {
  fields: string[];
  access: AccessRule;
  query?: {
    filterable?: string[] | null;
    searchable?: string[];
    sortable?: string[];
    defaultSort?: string;
  };
}

For current registries, access is the view's explicit access rule or the inherited table read.access when the view omits an override. query.searchable is likewise the effective search allowlist: the explicit view list when present, otherwise the compiler's eligible table search fields after masked fields are removed. An empty array means the named view does not support ?search; it does not mean the table has no rows. query.filterable contains the explicit named-view allowlist when authored, otherwise a q table's column-level .filterable() defaults. Raw Drizzle tables emit null when no closed filter allowlist can be derived, preserving their compatibility mode.

create, update, delete, and upsert mirror the preferred flat DSL from defineTable(...). The legacy crud block is still emitted for backwards compatibility with older consumers.

The registry is a projection of authored definitions, not a source of missing intent. In particular:

  • views lists the named projections available to the CMS. defaultView carries the authored read.defaultView. When named views exist without one, the registry emits requiresExplicitView: true; the CMS must show Choose a view without making a collection request. It must not silently choose the first accessible projection, because that would invent an access/shape contract. All Fields is absent whenever views is nonempty. A bare API collection request in this state returns VIEW_REQUIRED. defaultSort is unrelated; it controls ordering only.
  • displayColumn supplies the human-readable label used for records and referenced values. Set it explicitly when the table is a user-facing FK target.
  • crud, the flat write-operation metadata, and guards determine whether New and edit controls can exist and which inputs they expose.
  • actions and featureActions, together with action access and cms metadata, determine whether a control is record-bound, toolbar-placed, disabled by state, or intentionally hidden.

Named-view targets and FK lookups

An FK lookup targeting a table with no named views uses the ordinary collection route. Once the target declares named views, the CMS may automatically use only its authored, role-accessible defaultView. It calls GET /api/v1/<target>/views/<defaultView> and never retries a bare collection or silently chooses another view.

That default view is lookup-safe when its fields include both the target's primary key and displayColumn. If the default is missing, inaccessible to the current role, or omits either identity/label field, the CMS disables the lookup and displays an actionable explanation without making a data request.

For label typeahead, include the displayColumn in the effective query.searchable allowlist. An explicit named-view allowlist is authoritative, so the column does not also need .searchable(). When the effective searchable list is empty, the selector remains usable as a first-50 chooser; its search box is read-only and the CMS sends no ?search parameter.

Feature Actions

Standalone actions do not always belong to a single table. When an action is attached to a multi-table feature root, the compiler emits it under featureActions[featureName] instead of duplicating it across every table.

interface SchemaRegistry {
  featureActions: Record<string, ActionMeta[]>;
}

The CMS uses featureActions for toolbar-style actions that apply to the feature as a whole. Use cms.placement on standalone actions to control whether they show for the whole feature or only selected tables.

Action dialogs derive labels and controls from ActionMeta.inputFields plus the table registry. An input named sessionId is labeled Session. When a corresponding column has fkTarget: "sessions", the CMS renders a typeahead using the target table's displayColumn and the lookup read contract above; otherwise it renders a plain text input. This is another reason to author the schema-level .references() even when the action, rather than generic create CRUD, is the visible create flow.

Internal Tables

Tables without a defineTable() resource config are marked internal: true and hidden from the CMS sidebar. These are typically join tables or system tables.

ColumnMeta

Each column in the columns array:

interface ColumnMeta {
  name: string;        // Property name (camelCase)
  dbName: string;      // SQL column name (snake_case)
  type: "text" | "integer" | "real" | "blob";  // SQLite type
  mode?: "boolean";    // When an integer represents a boolean
  primaryKey: boolean;
  notNull: boolean;
  defaultValue?: string | number | boolean;
  fkTarget?: string;   // Target table name for FK columns
}

The compiler automatically includes the system columns (id, organizationId), the audit fields (createdAt, createdBy, modifiedAt, modifiedBy), and the soft-delete pair (deletedAt, deletedBy — when soft delete is enabled) at the beginning of every table's column list.

fkTarget — FK Resolution

Columns with an authored feature-table relationship have an fkTarget property indicating which table they reference. In the Quickback schema DSL, declare that relationship on the column:

sessionId: q.text().required().references(() => sessions.id)

The compiler also understands Drizzle's equivalent .references() form. A legacy defineTable({ references: { ... } }) map can override registry target metadata, but it does not create the database foreign key; prefer the schema-level relationship.

Do not rely on a column merely ending in Id. A same-named sibling table is a useful warning/suggestion when the explicit relationship is missing, but the name alone cannot establish author intent. CMS-ready definitions make every feature-table relationship explicit. Better Auth identifiers such as userId and organizationId are intentionally different: their tables live in AUTH_DB, so feature columns must not add cross-database .references() calls.

{
  "name": "vendorId",
  "type": "text",
  "fkTarget": "contact"
}

The CMS uses fkTarget to render typeahead/lookup inputs that search the correct table instead of showing raw IDs.

Input Hints

Tables with inputHints configured in defineTable() include an inputHints map in their metadata:

{
  "name": "invoice",
  "inputHints": {
    "status": "select",
    "sortOrder": "radio",
    "isPartialPaymentDisabled": "checkbox",
    "headerMessage": "textarea",
    "description": "richtext"
  }
}

The CMS reads these hints to render the appropriate form control for each field. The "richtext" hint renders a tiptap editor in edit mode and formatted HTML in view mode. See Input Hints for the full list of supported values.

Display Column

The displayColumn field tells the CMS which column to use as a human-readable label for a record. This is used in:

  • FK typeahead dropdowns (showing names instead of IDs)
  • Record titles in detail views
  • Breadcrumb labels

Auto-Detection

If you don't explicitly set displayColumn in your resource config, the compiler auto-detects it by scanning column names in priority order:

  1. name
  2. title
  3. label
  4. headline
  5. subject
  6. code
  7. displayName
  8. fullName
  9. description

The first match wins. If no candidate matches, the table has no display column and the CMS falls back to showing IDs.

For a table used in CMS selectors or record titles, prefer explicit config even when auto-detection currently finds the same column. It records which label is part of the UI contract and prevents a later schema change from silently changing that label.

Explicit Config

Set it explicitly in your table definition:

export default defineTable(contacts, {
  displayColumn: "companyName",
  // ...
});

FK Label Resolution

When a table has foreign key columns (ending in Id), the API enriches list responses with _label fields. For example, a roomTypeId column gets a corresponding roomType_label field containing the display column value from the referenced table.

The CMS uses these _label fields to show human-readable names in table cells and FK typeahead dropdowns instead of raw UUIDs.

roomTypeId: "rt_abc123"        → displayed as "Master Bedroom"
accountCodeId: "ac_xyz789"     → displayed as "4100 - Revenue"

The FK target table is resolved from the fkTarget property on each column. Declare a schema-level .references() relationship for feature-table ids, including apparently obvious cases such as roomTypeIdroomTypes. Do not make selector behavior depend on a naming heuristic. For a legacy metadata-only alias such as vendorIdcontact, the resource references map can override fkTarget, but it is not a substitute for a database FK when the two feature tables genuinely have one.

Pages

When features include definePage() files (in features/{name}/pages/), the compiler includes them in the registry under two fields:

  • pages — Map of page slug to full PageMeta object (data sources, layout, matching rules, page actions)
  • pagesByFeature — Map of feature name to array of page slugs belonging to that feature

The CMS reads these fields to render custom pages in the sidebar and route to the page renderer. See Custom Pages for the full definePage() API.

Next Steps

On this page