← migration

prisma → briven

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

prisma's strengths (typed clients, sql-agnostic schema files) and weaknesses (long cold starts, awkward complex queries) are both reasons people move. on briven you keep the typed-client feel — every query() / mutation() is typed via its Argsinterface — and trade prisma client's ORM-level helpers for a thin postgres query builder. the schema port is mechanical; the functions port is the place to think.

schema port — prisma DSL → briven DSL

prisma uses its own schema language (.prisma files). map decorators to briven column-builder calls:

// schema.prisma
model Post {
  id          String   @id @default(cuid())
  authorId    String
  title       String
  body        String
  published   Boolean  @default(false)
  views       Int      @default(0)
  metadata    Json?
  createdAt   DateTime @default(now())
  author      User     @relation(fields: [authorId], references: [id])
  @@index([authorId])
  @@index([published, authorId])
}

// 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(),
      body:      text().notNull(),
      published: boolean().notNull().default('false'),
      views:     bigint().notNull().default('0'),
      metadata:  jsonb<Record<string, unknown>>().nullable(),
      createdAt: timestamp().notNull().default('now()'),
    },
    indexes: [
      { columns: ['authorId'], unique: false },
      { columns: ['published', 'authorId'], unique: false },
    ],
  }),
});
  • @id.primaryKey(). prisma's @default(cuid()) / @default(uuid())don't carry over — briven mints ids in function code via ulid('prefix') from @briven/shared. ULIDs sort lexicographically by creation time, which is usually what you want anyway.
  • Intbigint(). prisma maps Int to int4 by default; briven defaults numeric columns to int8 to head off overflow. if you need int4 specifically, file an issue.
  • Json?jsonb<T>().nullable(). give the column a type arg so the function code gets typed reads.
  • DateTime @default(now()) timestamp().notNull().default('now()').
  • @relation(fields: […], references: […]) .references('table', 'column')on the fk column. briven doesn't generate the reverse-side accessor — query through ctx.db on the related table directly.
  • @@index([a, b]) → entry in the table's indexes array. partial / expression indexes (e.g. @@index([a], where: { … })) are a known gap.

enums

prisma enum declarations have no first-class briven equivalent today. two paths, depending on how strict you want the constraint:

  • application-side — column stays text(), the function code validates against a TypeScript union literal. less strict but flexible; matches how convex / nextauth migrations land.
  • database-side — apply the enum as a check-constraint via a raw-sql migration after briven deploy. briven preserves untouched user objects on re-deploy, so the constraint survives.

data export from prisma's postgres

prisma is one of several clients pointing at postgres — the dump/restore is the same as the raw-postgres playbook:

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

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

run briven deploy first so the briven schema is in place, then restore --data-only. that keeps briven's id naming + index naming in sync with what the briven dsl declared, instead of inheriting prisma's names.

prisma's migration history table (_prisma_migrations) doesn't carry — briven tracks its own migrations in _briven_migrations. drop the prisma table after the cutover.

functions port — PrismaClient calls → ctx.db chains

prisma client is generated; briven's ctx.db is a thin knex-style builder. the port pattern is: replace prisma.model.op with the equivalent builder chain.

// before — prisma handler
import { prisma } from './db';

export async function recentPostsByAuthor(authorId: string, limit = 50) {
  return prisma.post.findMany({
    where: { authorId, published: true },
    select: { id: true, title: true, createdAt: true },
    orderBy: { createdAt: 'desc' },
    take: Math.min(limit, 200),
  });
}

// 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));
});
  • findMany / findFirst / findUnique → builder chain ending in .first() for unique lookups.
  • create / update / delete insert / update / delete. upsert needs an explicit onConflict in raw sql today (known gap; an upsert helper is queued).
  • include / select with nested relations → no eager-join shortcut yet. either run two queries inside the same function (the function executes in one transaction, so the consistency is the same) or write a raw query for the joined shape.
  • prisma $transaction → not needed; every mutation() body runs in a single transaction by default.

auth port

prisma is unopinionated about auth — the user table is whatever you built. if it lines up with better-auth's columns (id, email, name, image, …) the nextauth → briven guide maps cleanly; if it's a custom shape, port the columns onto better-auth's expected shape before flipping traffic.

reactivity (new capability)

prisma queries are one-shot; the typical pattern is polling or websockets-on-the-side. on briven the same query() used over http auto-becomes reactive when consumed via @briven/react's useQuery. table-level NOTIFYs trigger re-runs — no code change needed.