← migration

drizzle → briven

port a drizzle-orm + postgres project onto briven. follow the ten-step playbook on /migration — this page covers only the drizzle-specific parts.

drizzle is the closest source shape to briven's schema dsl — both target postgres with typescript-first definitions. the schema port is mostly a search/replace; the functions port is replacing the drizzle db.select(...)chains with briven's ctx.db(...) chains (same query-builder shape).

schema port — direct mappings

drizzle column helpers map to briven helpers as follows:

// drizzle/schema.ts
import { pgTable, text, boolean, timestamp, integer, jsonb } from 'drizzle-orm/pg-core';

export const posts = pgTable('posts', {
  id: text('id').primaryKey(),
  authorId: text('author_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  published: boolean('published').notNull().default(false),
  views: integer('views').notNull().default(0),
  metadata: jsonb('metadata').$type<{ tags: string[] }>(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});

// briven/schema.ts
import { bigint, boolean, jsonb, schema, table, text, timestamp } from '@briven/cli/schema';

export default schema({
  posts: table({
    columns: {
      id:        text().primaryKey(),
      authorId:  text().notNull().references('users', 'id'),
      title:     text().notNull(),
      published: boolean().notNull().default('false'),
      views:     bigint().notNull().default('0'),
      metadata:  jsonb<{ tags: string[] }>().nullable(),
      createdAt: timestamp().notNull().default('now()'),
    },
  }),
});
  • drizzle integer() → briven bigint() (briven defaults to int8 for numeric counters to head off overflow).
  • drizzle jsonb<T>().$type<T>() → briven jsonb<T>(). type assertion lives on the column builder in both.
  • drizzle .defaultNow() → briven .default('now()') (we accept a string-literal sql default; now() is recognised verbatim).
  • drizzle .references(() => users.id) → briven .references('users', 'id'). drizzle's closure form preserves circular-ref ordering; briven resolves by name so the order doesn't matter.
  • drizzle uses snake_case column names in the second arg; briven derives the sql name from the property name (camelCase → snake_case) so you can drop the extra arg.

indexes

drizzle defines indexes via the third tuple arg on pgTable. briven uses an inline indexes array on the table def.

// drizzle
export const posts = pgTable('posts', { /* columns */ }, (t) => ({
  authorIdx: index('posts_author_idx').on(t.authorId),
  publishedAuthorIdx: index().on(t.published, t.authorId),
}));

// briven
posts: table({
  columns: { /* ... */ },
  indexes: [
    { columns: ['authorId'], unique: false },
    { columns: ['published', 'authorId'], unique: false },
  ],
});

drizzle's named indexes don't round-trip — briven generates names from (table, columns) so renaming a column auto-renames the index too. if a drizzle index was named for a specific reason (e.g. partial indexes via raw sql), open an issue; partial indexes are a known gap.

data export from drizzle's postgres

drizzle is just a query builder on top of postgres, so the export is the same as the raw-postgres playbook:

pg_dump --format=custom --no-owner --no-privileges \
  "$DRIZZLE_DATABASE_URL" > drizzle-dump-$(date +%Y%m%d).dump

pg_restore --no-owner --no-privileges \
  -d "$BRIVEN_PROJECT_DSN" drizzle-dump-$(date +%Y%m%d).dump

briven's migration applies a clean schema first; pg_restorewrites the data into the same tables. if the drizzle source had columns briven's dsl can't model yet (e.g. partial indexes, enum types, custom types), restore will warn — fix at the data level rather than retroactively changing the briven schema.

functions port — query builder is nearly identical

drizzle db and briven ctx.db share the postgres-query-builder shape (both lean on knex semantics). the port is a search/replace on the import + the handle name.

// before — drizzle handler
import { db } from './db';
import { posts, users } from './schema';
import { and, eq, desc } from 'drizzle-orm';

export async function recentPostsByAuthor(authorId: string, limit = 50) {
  return db
    .select({ id: posts.id, title: posts.title, createdAt: posts.createdAt })
    .from(posts)
    .where(and(eq(posts.authorId, authorId), eq(posts.published, true)))
    .orderBy(desc(posts.createdAt))
    .limit(limit);
}

// after — briven/functions/recentPostsByAuthor.ts
import { brivenError, query, type Ctx } from '@briven/cli/server';

interface Args { authorId: string; limit?: number }

export default query(async (ctx: Ctx, args: Args) => {
  if (!args.authorId)
    throw new brivenError('validation_failed', 'authorId required', { status: 400 });
  return ctx
    .db('posts')
    .select(['id', 'title', 'createdAt'])
    .where({ authorId: args.authorId, published: true })
    .orderBy('createdAt', 'desc')
    .limit(Math.min(args.limit ?? 50, 200));
});
  • drizzle db.select({a, b}) → briven ctx.db('table').select(['a', 'b']).
  • drizzle's and / eq / desc operators → briven uses object-literal where clauses + string-keyed orderBy(knex-style). drizzle's richer operators (e.g. ilike, arrayContains) land on briven via raw fragments — see /functions.
  • drizzle insert / update / delete → briven ctx.db(table).insert / update / delete — same shape.

auth port

drizzle ships no auth — you're running it next to better-auth, lucia, next-auth, or a hand-rolled session table. briven ships better-auth integrated; map your existing user / session columns into briven's control-plane shape via the nextauth → briven guide (the same column-mapping applies whichever lib generated the rows).

reactivity (new capability)

drizzle queries are one-shot. once on briven, wrap a read as a query() and the same call from @briven/react's useQuery auto-refetches on table-level NOTIFYs. no extra code on the function side.