Seeding

Everything a site needs to exist can be created from code: the custom-database schema, its records, static collections, your product catalogue, your blog posts and your coupons.

Two ways in, backed by the same functions:

  • The CLI — businessdash sync-data-model, sync-records, sync-content. Right at a terminal.
  • @businessdash/sdk/seed — plain async functions. Right everywhere else: a Nuxt module, a SvelteKit hook, an Angular builder, a Vite plugin, a Next instrumentation.ts, or a CI step that already holds the credentials. A command you have to shell out to and screen-scrape is not an API.

What can be seeded

TargetFunctionEndpoint
Custom-database schemapushDataModeldata-model/plan + draft-schema
Custom-database recordsseedRecordsdata-model/records
Static collections + rowsseedCollection, seedCollectionRowscollections, collections/{slug}/rows
Products, variants, cross-variantsseedProductsstorefront/products
Blog postsseedBlogPostsblog/posts
CouponsseedCouponscoupons

A whole seed in one file

// businessdash.seed.ts
import { defineSeed } from '@businessdash/sdk/seed'
import { schema } from './businessdash.data-model.config'

export default defineSeed({
  dataModel: schema,

  records: {
    projects: [{ title: 'Riverside retrofit', status: 'active' }],
  },

  products: [
    {
      name: 'Thermal survey',
      description: 'Whole-property infrared survey.',
      variants: [
        {
          name: 'Standard',
          price: 249,
          crossVariantTitles: [
            {
              title: 'Turnaround',
              selectionMode: 'single',
              options: [
                { value: '5 working days', price: 0 },
                { value: 'Next day', price: 95 },
              ],
            },
          ],
        },
      ],
    },
  ],

  posts: [
    { title: 'Why retrofit surveys pay for themselves', slug: 'retrofit-surveys', content: '…' },
  ],

  coupons: [{ code: 'WELCOME10', discountType: 'percent', value: 10 }],
})

Run it from wherever your framework lets you run code at build time:

import { runSeed, seedTargetFromEnv } from '@businessdash/sdk/seed'
import seed from './businessdash.seed'

await runSeed(seedTargetFromEnv(), seed, { onProgress: console.log })

seedTargetFromEnv() reads the same three variables the CLI does — BD_API_KEY, BD_SITE_ID and BD_PACKAGE_API_BASE_URL (or NEXT_PUBLIC_BD_PACKAGE_API_BASE_URL) — so a project with a working CLI needs no new configuration.

Or run the same file from a terminal:

npx tsx node_modules/@businessdash/sdk/dist/cli.js seed
npx tsx node_modules/@businessdash/sdk/dist/cli.js seed --dry-run

--dry-run reports what each step would do and uploads nothing.

Four behaviours worth knowing before you run it

These are deliberate, and each exists because the alternative fails badly in an unattended deploy.

It plans before it pushes, always

pushDataModel computes the migration plan even when it is going to push, because a push whose consequences nobody computed is how a destructive migration reaches a live database. dryRun: true computes the plan and uploads nothing — the right thing to run on a pull request.

It refuses a destructive plan by default

runSeed throws when the plan contains destructive or exposure changes, and names the change rather than just refusing. The usual caller here is an automated deploy, and an automated deploy should not be the thing that drops a customer's column. If you have already decided, pass failOnDangerousPlan: false.

A push does not apply

Schemas land in the draft slot. Promotion stays a deliberate step in the dashboard, under Site Builder → Site Data → Database. Nothing in this module can promote a draft.

Re-running is safe

An existing coupon code or post slug is reported, not thrown — so a conflict on the third of ten does not abandon the remaining seven. A bad API key still throws, because a half-seeded store that reports success is worse than an obvious failure.

Products are not pushed to Stripe

seedProducts writes your own tables. Pushing the catalogue to your Stripe account is a separate, explicit call:

await client.storefront.syncToStripe()

It is separate because it mints objects in an external account you are billed against, and that should never be a side effect of a seed script.

Products are also created unpublished unless you pass isLive: true, and posts unpublished unless you pass publishNow: true. Seeding a site should not push a half-configured catalogue to a live storefront, or drafts to every follower's inbox.

Scopes

SeedingScope
Schema, recordsdata_model:write
Static collectionscollections:write
Productsstorefront:write
Blog postsblog:write
Couponsstorefront:write

blog:write is not publishable — authoring a post is an operator action, never something a browser token should be able to do.