page.tsx247 lines · main
1import Link from 'next/link';
2
3import { DocsShell } from '../../../components/shell';
4
5export const metadata = { title: 'migration · prisma → briven' };
6
7export default function PrismaMigrationPage() {
8 return (
9 <DocsShell>
10 <p className="font-mono text-xs text-[var(--color-text-muted)]">
11 <Link href="/migration" className="hover:text-[var(--color-text)]">
12 ← migration
13 </Link>
14 </p>
15 <h1 className="mt-2 font-mono text-2xl tracking-tight">prisma → briven</h1>
16 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
17 port a prisma + postgres project onto briven. follow the ten-step playbook on{' '}
18 <Link href="/migration" className="underline underline-offset-2">
19 /migration
20 </Link>{' '}
21 — this page covers only the prisma-specific parts.
22 </p>
23
24 <div className="mt-4 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]">
25 prisma&apos;s strengths (typed clients, sql-agnostic schema files) and weaknesses (long
26 cold starts, awkward complex queries) are both reasons people move. on briven you keep
27 the typed-client feel — every <code>query()</code> / <code>mutation()</code> is typed via
28 its <code>Args</code> interface — and trade prisma client&apos;s ORM-level helpers for a
29 thin postgres query builder. the schema port is mechanical; the functions port is the
30 place to think.
31 </div>
32
33 <Section title="schema port — prisma DSL → briven DSL">
34 <p>
35 prisma uses its own schema language (<code>.prisma</code> files). map decorators to
36 briven column-builder calls:
37 </p>
38 <Snippet>{`// schema.prisma
39model Post {
40 id String @id @default(cuid())
41 authorId String
42 title String
43 body String
44 published Boolean @default(false)
45 views Int @default(0)
46 metadata Json?
47 createdAt DateTime @default(now())
48 author User @relation(fields: [authorId], references: [id])
49 @@index([authorId])
50 @@index([published, authorId])
51}
52
53// briven/schema.ts
54import { bigint, boolean, jsonb, schema, table, text, timestamp } from '@briven/cli/schema';
55
56export default schema({
57 posts: table({
58 columns: {
59 id: text().primaryKey(),
60 authorId: text().notNull().references('users', 'id'),
61 title: text().notNull(),
62 body: text().notNull(),
63 published: boolean().notNull().default('false'),
64 views: bigint().notNull().default('0'),
65 metadata: jsonb<Record<string, unknown>>().nullable(),
66 createdAt: timestamp().notNull().default('now()'),
67 },
68 indexes: [
69 { columns: ['authorId'], unique: false },
70 { columns: ['published', 'authorId'], unique: false },
71 ],
72 }),
73});`}</Snippet>
74 <ul className="list-disc pl-5">
75 <li>
76 <code>@id</code> → <code>.primaryKey()</code>. prisma&apos;s <code>@default(cuid())</code>{' '}
77 / <code>@default(uuid())</code> don&apos;t carry over — briven mints ids in function
78 code via <code>ulid(&apos;<em>prefix</em>&apos;)</code> from{' '}
79 <code>@briven/shared</code>. ULIDs sort lexicographically by creation time, which is
80 usually what you want anyway.
81 </li>
82 <li>
83 <code>Int</code> → <code>bigint()</code>. prisma maps <code>Int</code> to int4 by
84 default; briven defaults numeric columns to int8 to head off overflow. if you need
85 int4 specifically, file an issue.
86 </li>
87 <li>
88 <code>Json?</code> → <code>jsonb&lt;T&gt;().nullable()</code>. give the column a type
89 arg so the function code gets typed reads.
90 </li>
91 <li>
92 <code>DateTime @default(now())</code> →{' '}
93 <code>timestamp().notNull().default(&apos;now()&apos;)</code>.
94 </li>
95 <li>
96 <code>@relation(fields: […], references: […])</code> →{' '}
97 <code>.references(&apos;table&apos;, &apos;column&apos;)</code> on the fk column.
98 briven doesn&apos;t generate the reverse-side accessor — query through{' '}
99 <code>ctx.db</code> on the related table directly.
100 </li>
101 <li>
102 <code>@@index([a, b])</code> → entry in the table&apos;s <code>indexes</code> array.
103 partial / expression indexes (e.g. <code>@@index([a], where: {'{ … }'})</code>) are a
104 known gap.
105 </li>
106 </ul>
107 </Section>
108
109 <Section title="enums">
110 <p>
111 prisma <code>enum</code> declarations have no first-class briven equivalent today. two
112 paths, depending on how strict you want the constraint:
113 </p>
114 <ul className="list-disc pl-5">
115 <li>
116 <strong>application-side</strong> — column stays <code>text()</code>, the function
117 code validates against a TypeScript union literal. less strict but flexible; matches
118 how convex / nextauth migrations land.
119 </li>
120 <li>
121 <strong>database-side</strong> — apply the enum as a check-constraint via a raw-sql
122 migration after <code>briven deploy</code>. briven preserves untouched user objects on
123 re-deploy, so the constraint survives.
124 </li>
125 </ul>
126 </Section>
127
128 <Section title="data export from prisma's postgres">
129 <p>
130 prisma is one of several clients pointing at postgres — the dump/restore is the same as
131 the <Link href="/migration/postgres" className="underline underline-offset-2">raw-postgres playbook</Link>:
132 </p>
133 <Snippet>{`pg_dump --format=custom --no-owner --no-privileges \\
134 "$PRISMA_DATABASE_URL" > prisma-dump-$(date +%Y%m%d).dump
135
136pg_restore --no-owner --no-privileges --data-only \\
137 -d "$BRIVEN_PROJECT_DSN" prisma-dump-$(date +%Y%m%d).dump`}</Snippet>
138 <p>
139 run <code>briven deploy</code> first so the briven schema is in place, then restore{' '}
140 <code>--data-only</code>. that keeps briven&apos;s id naming + index naming in sync with
141 what the briven dsl declared, instead of inheriting prisma&apos;s names.
142 </p>
143 <p>
144 prisma&apos;s migration history table (<code>_prisma_migrations</code>) doesn&apos;t
145 carry — briven tracks its own migrations in <code>_briven_migrations</code>. drop the
146 prisma table after the cutover.
147 </p>
148 </Section>
149
150 <Section title="functions port — PrismaClient calls → ctx.db chains">
151 <p>
152 prisma client is generated; briven&apos;s <code>ctx.db</code> is a thin knex-style
153 builder. the port pattern is: replace <code>prisma.<em>model</em>.<em>op</em></code>{' '}
154 with the equivalent builder chain.
155 </p>
156 <Snippet>{`// before — prisma handler
157import { prisma } from './db';
158
159export async function recentPostsByAuthor(authorId: string, limit = 50) {
160 return prisma.post.findMany({
161 where: { authorId, published: true },
162 select: { id: true, title: true, createdAt: true },
163 orderBy: { createdAt: 'desc' },
164 take: Math.min(limit, 200),
165 });
166}
167
168// after — briven/functions/recentPostsByAuthor.ts
169import { brivenError, query, type Ctx } from '@briven/cli/server';
170
171interface Args { authorId: string; limit?: number }
172
173export default query(async (ctx: Ctx, args: Args) => {
174 if (!args.authorId)
175 throw new brivenError('validation_failed', 'authorId required', { status: 400 });
176 return ctx
177 .db('posts')
178 .select(['id', 'title', 'createdAt'])
179 .where({ authorId: args.authorId, published: true })
180 .orderBy('createdAt', 'desc')
181 .limit(Math.min(args.limit ?? 50, 200));
182});`}</Snippet>
183 <ul className="list-disc pl-5">
184 <li>
185 <code>findMany</code> / <code>findFirst</code> / <code>findUnique</code> → builder
186 chain ending in <code>.first()</code> for unique lookups.
187 </li>
188 <li>
189 <code>create</code> / <code>update</code> / <code>delete</code> →{' '}
190 <code>insert / update / delete</code>. <code>upsert</code> needs an explicit{' '}
191 <code>onConflict</code> in raw sql today (known gap; an upsert helper is queued).
192 </li>
193 <li>
194 <code>include</code> / <code>select</code> with nested relations → no eager-join
195 shortcut yet. either run two queries inside the same function (the function executes
196 in one transaction, so the consistency is the same) or write a raw query for the
197 joined shape.
198 </li>
199 <li>
200 prisma <code>$transaction</code> → not needed; every <code>mutation()</code> body
201 runs in a single transaction by default.
202 </li>
203 </ul>
204 </Section>
205
206 <Section title="auth port">
207 <p>
208 prisma is unopinionated about auth — the user table is whatever you built. if it lines
209 up with better-auth&apos;s columns (<code>id, email, name, image, …</code>) the{' '}
210 <Link href="/migration/nextauth" className="underline underline-offset-2">
211 nextauth → briven
212 </Link>{' '}
213 guide maps cleanly; if it&apos;s a custom shape, port the columns onto better-auth&apos;s
214 expected shape before flipping traffic.
215 </p>
216 </Section>
217
218 <Section title="reactivity (new capability)">
219 <p>
220 prisma queries are one-shot; the typical pattern is polling or websockets-on-the-side.
221 on briven the same <code>query()</code> used over http auto-becomes reactive when
222 consumed via <code>@briven/react</code>&apos;s <code>useQuery</code>. table-level
223 NOTIFYs trigger re-runs — no code change needed.
224 </p>
225 </Section>
226 </DocsShell>
227 );
228}
229
230function Section({ title, children }: { title: string; children: React.ReactNode }) {
231 return (
232 <section className="mt-10">
233 <h2 className="font-mono text-lg">{title}</h2>
234 <div className="mt-2 space-y-3 font-mono text-sm text-[var(--color-text-muted)]">
235 {children}
236 </div>
237 </section>
238 );
239}
240
241function Snippet({ children }: { children: string }) {
242 return (
243 <pre className="overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs">
244 <code>{children}</code>
245 </pre>
246 );
247}