Collections and CLI Changes

A collection is the database boundary in a 0.9.50 declaration. It names a set of records, owns its fields, and becomes one object in databaseSchema. It also retains enough compile-time information for InferCollectionValues<typeof collection> to type a hand-built form or record component. See Inferred Types and Custom Forms.

Declare the first collection

Create bd.data-model.config.ts in the project root:

import {
  bd,
  collection,
  createFormSchemaOrchestrator,
} from '@businessdash/sdk'

const projects = collection('projects', {
  description: 'Projects visible in the customer portal.',
  visibility: 'private',
  labelIdentifierFieldName: 'title',
  idType: 'uuid',
  fields: {
    title: bd.text().required(),
    active: bd.boolean().default(true),
  },
})

export const definition = createFormSchemaOrchestrator([projects])

// Keep database + generated-form candidates together.
export default definition

collection(tableName, options) derives singular/plural names and labels. For example, projects becomes the singular collection id project.

OptionMeaning
fieldsRequired map from field name to a bd.* builder
description / iconHuman-facing collection metadata
visibilityprivate (default), authenticated, or public
labelIdentifierFieldNameField used as a record's display label
idTypePer-collection primary key strategy: uuid or integer
rlsPoliciesLocal row-level policy metadata

If the collection has no primary key, createSchema() injects id. The orchestrator's defaultIdType controls the default:

createFormSchemaOrchestrator([projects], {
  defaultIdType: 'integer',
})

Direct schema versus orchestration

Use createSchema() when only a database artifact is needed:

export default createSchema({
  collections: [projects],
})

Use createFormSchemaOrchestrator() when the same declaration also needs uiFormTree, generated form candidates, form-control settings, or optional feature tags:

const definition = createFormSchemaOrchestrator([projects])

export default definition
export const projectTree = definition.uiFormTree

The CLI recognizes the orchestrator result, extracts databaseSchema, and can include selected generatedForms. It never uploads uiFormTree. Review Definitions and Runtime for the contract boundary.

How a schema change moves

edit TypeScript
      │
      ▼
compile and checksum
      │
      ▼
compare with promoted Live model
      │
      ▼
preview migration plan
      │
      ▼
push draft
      │
      ▼
human review and promotion

An API key can propose a draft. Promotion remains a dashboard action.

Inspect the local artifact

npx bd print-data-model

The CLI loads the default export, calls toJSONSchema(), prints the normalized artifact, and computes its checksum without making a network request.

The configuration lookup order is:

bd.data-model.config.ts
bd.data-model.config.mjs
bd.data-model.config.js

Override it when needed:

npx bd print-data-model --config ./config/customer-schema.ts

Compare with Live

npx bd view-data-model
npx bd view-data-model --diff

Adopt a dashboard-authored model:

npx bd view-data-model --out bd.data-model.config.ts

Review generated names and identities before making that file authoritative.

Preview and push

npx bd sync-data-model --dry-run

The dry run compiles local source, compares it with the promoted Live model, and prints the migration plan without creating a draft.

npx bd sync-data-model \
  --note "Add project intake collections"

When the orchestrator supplies generated form candidates, an interactive run offers a numbered multi-select. Press Enter to select all. CI/non-interactive runs include all candidates by default:

# Include every candidate
npx bd sync-data-model --forms

# Include only named candidates
npx bd sync-data-model --forms project-form,task-form

# Propose collections without generated forms
npx bd sync-data-model --no-forms

For each field, generation uses its explicit .formElement(...) or the first entry in recommendedFormElements. The SDK translates declaration names to the persisted renderer type, such as short_text → text, dropdown → select, and radio_group → radio.

Review and promote the resulting draft in:

Site Builder → Site Data → Database

Generated forms are created or refreshed only when the schema is promoted, and they are always inactive. Review each form and activate it separately after confirming its collections and create_records action.

Additive, convergent, and destructive changes

By default, a promoted collection omitted from local source is preserved. This makes ordinary syncs additive and convergent.

Use --prune only when absence should propose removal:

npx bd sync-data-model --dry-run --prune
npx bd sync-data-model --prune --note "Remove retired collections"

Always inspect a prune plan before posting it. Visibility changes also deserve explicit review because they change data exposure.

Identities and rename safety

Schema diffs use stable collection, field, and option identities to distinguish a rename from removal plus addition. Fluent collection() declarations derive deterministic collection, field, injected-id, junction, and generated-form identities, so evaluating an unchanged file converges instead of proposing a drop and recreate.

An explicit identity from view-data-model --out remains authoritative when adopting an existing dashboard-built model.

Wipe collection records safely

wipe-collections removes records from named live custom collections. It does not delete the collection definitions, fields, generated forms, or your local schema config.

Start with a preview:

npx bd wipe-collections \
  --collections category,feature_group,feature \
  --dry-run

The preview resolves singular names, plural names, or stable collection UUIDs and reports active records, soft-deleted records, and incoming/outgoing relationship links. Targets are always explicit: the command has no implicit all-collections mode.

Run without --dry-run only from an interactive terminal:

npx bd wipe-collections \
  --collections category,feature_group,feature

The CLI then:

  1. asks for an initial yes;
  2. asks you to type a plan-specific phrase exactly;
  3. exports collection and field metadata, record JSONB values, timestamps, soft-delete state, and relationship links;
  4. saves the export under Media → Database Backups → Collection Wipes;
  5. verifies the stored object's byte length and SHA-256 metadata;
  6. rechecks the collection state in a serializable transaction; and
  7. hard-deletes only the record and relationship IDs contained in that backup.

Backup Media rows use an authenticated download bridge rather than a public R2 URL. Opening the file requires current media.read or media.manage access.

There is intentionally no --yes bypass. A non-interactive process may run the --dry-run preview, but it cannot execute a wipe. The Package API also requires a secret, user-bound key with metadata:write_records; the bound user must also hold metadata.manage and media.manage. Publishable keys and unbound service keys are rejected.

If a record value or relationship changes after preview, the plan token changes and execution stops. If it changes while the backup is being written, the verified backup remains in Media but no rows are deleted; preview again before retrying.

Command summary

CommandPurpose
view-data-modelRead the promoted model
view-data-model --diffCompare local and promoted models
view-data-model --out pathGenerate a local config from Live
print-data-modelCompile and print the local artifact and checksum
sync-data-model --dry-runPreview a migration plan
sync-data-modelPush a draft schema
sync-data-model --forms [slugs]Include all or selected inactive generated-form drafts
sync-data-model --no-formsPush collections without generated forms
sync-data-model --pruneInclude removals in the proposal
sync-recordsConverge seed records after the schema exists
wipe-collections --collections a,b --dry-runPreview record and relationship deletion
wipe-collections --collections a,bBack up, verify, reconfirm, and wipe named collection records

Next: Primitive Builders.