# 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 compile` and the compiler outputs `schema-registry.json` alongside your compiled API files.

To disable generation:

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

## Output Shape

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

```json
{
  "generatedAt": "2026-02-14T06:26:53.554Z",
  "generatedBy": "quickback-compiler",
  "version": "1.0.0",
  "singleTenant": false,
  "features": { ... },
  "tables": { ... },
  "tablesByFeature": { ... },
  "featureActions": { ... },
  "pages": { ... },
  "pagesByFeature": { ... }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `generatedAt` | `string` | ISO timestamp of generation |
| `generatedBy` | `string` | Always `"quickback-compiler"` |
| `version` | `string` | Compiler version used |
| `singleTenant` | `boolean?` | When `true`, no organization layer |
| `features` | `Record<string, string[]>` | Feature name to file list mapping |
| `tables` | `Record<string, TableMeta>` | Table name to full metadata |
| `tablesByFeature` | `Record<string, string[]>` | Feature name to table name list |
| `featureActions` | `Record<string, ActionMeta[]>` | Feature name to standalone actions not owned by one table |
| `pages` | `Record<string, PageMeta>` | Page slug to full page metadata |
| `pagesByFeature` | `Record<string, string[]>` | Feature name to page slug list |

## TableMeta

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

```typescript
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 only updatable via actions
  };
  masking: Record<string, MaskingRule>;  // Per-field masking rules
  views: Record<string, ViewConfig>;     // Named column projections
  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
}
```

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

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

```typescript
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.

> **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:

```typescript
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 ending in `Id` may have a `fkTarget` property indicating which table they reference. This is resolved in priority order:

1. **Explicit `references`** — from your `defineTable()` config (highest priority)
2. **Drizzle `.references()`** — parsed from schema source code
3. **Convention** — strip `Id` suffix, match table name directly

```json
{
  "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:

```json
{
  "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](/compiler/definitions/schema#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.

### Explicit Config

Set it explicitly in your table definition:

```typescript
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. For simple cases like `roomTypeId` → `roomType`, the compiler auto-detects it. For non-obvious mappings (e.g., `vendorId` → `contact`), use explicit [`references`](/compiler/definitions/schema#references) in your `defineTable()` config.

## 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](/stack/cms/pages) for the full `definePage()` API.

## Single-Tenant Mode

When `singleTenant` is `true` in the registry (set when the project config has `features.organizations: false`), the CMS skips the organization gate and reads the user's role directly from `user.role` on the session instead of from organization membership.

## Next Steps

- **[Connecting](/stack/cms/connecting)** — Demo mode vs. live mode setup
- **[Schema Format Reference](/stack/cms/schema-format)** — Full TypeScript types
