Seed data

One file that decides what a fresh copy of your app contains.

Every runtime that boots your project starts empty, and an empty app is hard to look at. database/seed.ts is the one file that fixes that: it holds the rows a fresh database gets after its schema is applied, so a preview, a branch, an agent working in the cloud, and a design capture all open on the same app with the same content in it.

It is the only thing in the project that answers “what does this app look like when it has data”. Nothing else needs to.

The file

It is data, not a program. A default export keyed by table name, with rows in insert order so a parent lands before anything referencing it:

import type { SeedData } from '@take-out/database'
import type * as schema from './schema'
export default {
userPublic: [{ id: 'seed-user-maya', name: 'Maya Okafor', username: 'maya' }],
post: [{ id: 'seed-post-1', authorId: 'seed-user-maya', text: 'First!' }],
} satisfies SeedData<typeof schema>

Three rules make it work everywhere it runs:

  • Give every row an explicit id. Inserts are per-row and skip on conflict, so applying the same seed twice changes nothing, and adding rows later back-fills only the new ones.
  • Keep it importable with nothing but standard library and import type. No Drizzle, no db, no query helpers. The runtime that loads it has no database connection yet.
  • Never seed a derived value. A rollup column like post.commentCount is maintained by triggers over the rows you seed below it. Seeding the count too doubles it.

Where it runs

The seed applies once at a runtime’s cold boot, after the schema is in place. In a browser preview that is the preview worker; in a server runtime it is the --seed flag on database/migrate.ts. Both apply the same rows with the same insert semantics, so there is no second definition of “the demo world” to keep in step.

A seed is part of the schema revision it belongs to, so it is not best-effort. If the file fails to load, names a table your current schema does not have, or contains something that is not a row, boot stops and says which table and which row. Fix the seed rather than working around it: a runtime that boots with half a world is worse than one that refuses.

Why one file matters this much

Contrast runs your app in a lot of places, and each one gets its own database. Without a single definition of the starting rows, each of those places drifts on its own and you end up debugging the environment instead of the app.

The clearest case is Design. Its screen captures render your app on a private runtime rather than the preview you have been clicking around in, precisely so the pictures show your app rather than whatever state you left that preview in. That only produces something worth looking at because the private runtime boots into the same world as everything else. Agents get the same benefit: one working in the cloud sees the app you see, so it can tell a real empty state from an empty database.

Changing the seed

Editing the file does not rewrite a database that already exists. The rows apply at cold boot, so to see a changed seed, restart the runtime from Run → Restart. Changing the shape of a table is a schema change first: run Generate and Migrate, then update the seed to match.

Ready?

Create a web, iOS, and Android app in minutes with agents working alongside you.