Seeding
@cfast/db includes a schema-driven seed generator that introspects your Drizzle schema to produce realistic test data. @faker-js/faker is bundled — you never need to install or import it. All seed functionality lives under the @cfast/db/seed entrypoint so tree-shaking keeps faker out of your production bundle.
Quick start
Section titled “Quick start”import { seed } from "@cfast/db/seed";import { createDb } from "@cfast/db";import * as schema from "./schema";
const db = createDb({ d1: env.DB, schema, grants: [], user: null });await seed(db);That’s it. seed(db) introspects the schema from the Db instance, topologically sorts tables by FK dependencies, generates realistic data, and inserts rows in the correct order via db.unsafe().
Column-level .seed() method
Section titled “Column-level .seed() method”Chain .seed(fn) on any Drizzle column builder to replace the auto-inferred generator:
import { text, integer } from "drizzle-orm/sqlite-core";import { table } from "@cfast/db/seed";
export const posts = table("posts", { id: text("id").primaryKey(), title: text("title").notNull().seed((f) => f.lorem.sentence()), slug: text("slug").notNull().seed((f) => f.lorem.slug()), views: integer("views").notNull().seed((f) => f.number.int({ min: 0, max: 10000 })), authorId: text("author_id").notNull().references(() => users.id),}).seed({ count: 10 });The .seed() method is available on every SQLite column builder (text(), integer(), real(), blob()). It accepts a (faker, ctx) => value function called once per generated row and returns this for chaining.
Table-level .seed() method
Section titled “Table-level .seed() method”Use table() from @cfast/db/seed instead of sqliteTable() to get a .seed() method on the table:
import { table } from "@cfast/db/seed";import { text } from "drizzle-orm/sqlite-core";
export const users = table("users", { id: text("id").primaryKey(), email: text("email").notNull(), name: text("name").notNull(),}).seed({ count: 5 });
export const posts = table("posts", { id: text("id").primaryKey(), title: text("title").notNull(), authorId: text("author_id").notNull().references(() => users.id),}).seed({ count: 3, per: users });table() is a drop-in replacement for sqliteTable() — the returned table is a standard Drizzle table in every way. With { count: 3, per: users }, the engine generates 3 posts per user. If there are 5 users, you get 15 posts total, each with authorId automatically set to the parent user’s id.
Deprecated wrapper functions
Section titled “Deprecated wrapper functions”The older seedConfig() and tableSeed() wrapper functions still work and write to the same internal registries. They are interchangeable with the .seed() methods but considered deprecated:
// Deprecated -- prefer .seed() method API aboveimport { seedConfig, tableSeed } from "@cfast/db/seed";import { sqliteTable, text } from "drizzle-orm/sqlite-core";
const posts = tableSeed(sqliteTable("posts", { title: seedConfig(text("title"), f => f.lorem.sentence()),}), { count: 5 });Column type auto-inference
Section titled “Column type auto-inference”When no .seed() is present, the engine picks a generator based on the Drizzle column type:
| Column type | Generated value |
|---|---|
text (PK) | faker.string.uuid() |
text | faker.lorem.words(3) |
integer | faker.number.int({ min: 0, max: 10000 }) |
real | faker.number.float({ min: 0, max: 1000 }) |
integer (boolean mode) | faker.datatype.boolean() |
integer (timestamp mode) | faker.date.recent() |
blob / buffer | faker.string.alphanumeric(16) |
| Nullable column | null ~10% of the time |
Columns with .$defaultFn() or a static .default() are skipped entirely — Drizzle fills them at insert time.
Auth table detection
Section titled “Auth table detection”Tables named users get special handling: the first five rows receive deterministic emails (admin@example.com, user@example.com, editor@example.com, viewer@example.com, moderator@example.com), and the name column uses faker.person.fullName(). This makes it easy to log in as different roles during development.
Relational generation with per
Section titled “Relational generation with per”The per option on .seed() creates one-to-many relationships automatically. The engine resolves the foreign key between the child table and the per parent, filling it with the parent row’s primary key.
Grandchild chains
Section titled “Grandchild chains”Chain per across multiple levels for deep hierarchies:
export const users = table("users", { ... }).seed({ count: 3 });export const posts = table("posts", { ... }).seed({ count: 2, per: users });export const comments = table("comments", { ... }).seed({ count: 4, per: posts });This produces 3 users, 6 posts (2 per user), and 24 comments (4 per post). The topological sort ensures users are seeded before posts, and posts before comments.
Parent context: ctx
Section titled “Parent context: ctx”The second argument to a .seed() function is a SeedContext with four properties:
ctx.parent
Section titled “ctx.parent”The parent row when the table uses per. undefined for root tables:
const children = table("children", { id: text("id").primaryKey(), parentId: text("parent_id").notNull().references(() => parents.id), label: text("label").seed((_f, ctx) => `Child of ${ctx.parent?.name}`, ),}).seed({ count: 2, per: parents });ctx.ref(table)
Section titled “ctx.ref(table)”Pick a random already-generated row from any table. Useful for cross-references that are not direct FKs:
const reviews = table("reviews", { id: text("id").primaryKey(), productId: text("product_id").notNull().references(() => products.id), reviewerNote: text("reviewer_note").seed((_f, ctx) => { const product = ctx.ref(products); return `Review for ${product.name}`; }),}).seed({ count: 10 });ctx.index
Section titled “ctx.index”Zero-based position within the current batch (per-parent or global):
text("slug").notNull().seed((_f, ctx) => `post-${ctx.index}`);ctx.all(table)
Section titled “ctx.all(table)”Returns all generated rows for a table (available after that table has been seeded). Useful for summary or aggregate seed data:
integer("total_items").notNull().seed((_f, ctx) => ctx.all(items).length);Many-to-many with deduplication
Section titled “Many-to-many with deduplication”Join tables (tables with 2+ foreign keys) are automatically deduplicated. If the engine would generate a duplicate (postId, tagId) combination for a postTags table, the duplicate row is silently dropped:
export const postTags = table("post_tags", { id: text("id").primaryKey(), postId: text("post_id").notNull().references(() => posts.id), tagId: text("tag_id").notNull().references(() => tags.id),}).seed({ count: 5 });// If only 3 unique (post, tag) combos exist, you get 3 rows, not 5.Conditional seeding
Section titled “Conditional seeding”Use .seed() with ctx.index or ctx.parent for role-based or conditional data:
const users = table("users", { id: text("id").primaryKey(), email: text("email").notNull().seed((f, ctx) => { const roles = ["admin", "editor", "viewer"]; if (ctx.index < roles.length) return `${roles[ctx.index]}@example.com`; return f.internet.email(); }), role: text("role").notNull().seed((_f, ctx) => { const roles = ["admin", "editor", "viewer"]; return roles[ctx.index % roles.length]; }),}).seed({ count: 5 });Single-table override
Section titled “Single-table override”Seed a single table without running the full engine. This is useful for quick one-off data in tests:
import { createSingleTableSeed } from "@cfast/db/seed";
const singleSeed = createSingleTableSeed(schema, posts, 5);await singleSeed.run(db);Other tables in the schema get zero rows. Foreign key parents are still generated if needed for referential integrity.
SQL transcript output
Section titled “SQL transcript output”Pass { transcript: "/path/to/seed.sql" } to seed() or .run() to write the generated INSERT statements to a file:
import { seed } from "@cfast/db/seed";await seed(db, { transcript: "./seed-transcript.sql" });The transcript file contains one INSERT INTO statement per row, in topological order. In Workers environments where node:fs is unavailable, the transcript write is silently skipped.
Static seed with defineSeed()
Section titled “Static seed with defineSeed()”For hand-authored fixture data (not generated), use defineSeed() with explicit row arrays:
import { defineSeed } from "@cfast/db/seed";import { createDb } from "@cfast/db";import * as schema from "./schema";
const mySeed = defineSeed({ entries: [ { table: schema.users, rows: [ { id: "u-1", email: "ada@example.com", name: "Ada" }, { id: "u-2", email: "grace@example.com", name: "Grace" }, ], }, { table: schema.posts, rows: [ { id: "p-1", authorId: "u-1", title: "Hello" }, ], }, ],});
const db = createDb({ d1, schema, grants: [], user: null });await mySeed.run(db);Row types are inferred from the Drizzle table, so typos in column names are caught by tsc. Entries are inserted in list order — place parent tables before children.
See also
Section titled “See also”@cfast/db— full package reference- Transactions vs Batch — atomic writes and read-modify-write patterns