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.
| Option | Meaning |
|---|---|
fields | Required map from field name to a bd.* builder |
description / icon | Human-facing collection metadata |
visibility | private (default), authenticated, or public |
labelIdentifierFieldName | Field used as a record's display label |
idType | Per-collection primary key strategy: uuid or integer |
rlsPolicies | Local 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:
- asks for an initial
yes; - asks you to type a plan-specific phrase exactly;
- exports collection and field metadata, record JSONB values, timestamps, soft-delete state, and relationship links;
- saves the export under Media → Database Backups → Collection Wipes;
- verifies the stored object's byte length and SHA-256 metadata;
- rechecks the collection state in a serializable transaction; and
- 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
| Command | Purpose |
|---|---|
view-data-model | Read the promoted model |
view-data-model --diff | Compare local and promoted models |
view-data-model --out path | Generate a local config from Live |
print-data-model | Compile and print the local artifact and checksum |
sync-data-model --dry-run | Preview a migration plan |
sync-data-model | Push a draft schema |
sync-data-model --forms [slugs] | Include all or selected inactive generated-form drafts |
sync-data-model --no-forms | Push collections without generated forms |
sync-data-model --prune | Include removals in the proposal |
sync-records | Converge seed records after the schema exists |
wipe-collections --collections a,b --dry-run | Preview record and relationship deletion |
wipe-collections --collections a,b | Back up, verify, reconfirm, and wipe named collection records |
Next: Primitive Builders.