page.tsx188 lines · main
1import Link from 'next/link';
2
3import { DocsShell } from '../../../components/shell';
4
5export const metadata = { title: 'migration · postgres → briven' };
6
7export default function PostgresMigrationPage() {
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">raw postgres / drizzle / prisma → briven</h1>
16 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
17 the straightest path. you already have a postgres schema; briven gives you reactive
18 queries, function hosting, and a managed deploy story on top of it. follow the
19 ten-step playbook on{' '}
20 <Link href="/migration" className="underline underline-offset-2">
21 /migration
22 </Link>{' '}
23 — this page documents the postgres-specific parts.
24 </p>
25
26 <Section title="schema port — drizzle / prisma → briven dsl">
27 <p>your existing column types map directly. drizzle:</p>
28 <Snippet>{`// drizzle
29export const notes = pgTable('notes', {
30 id: text('id').primaryKey(),
31 body: text('body').notNull(),
32 authorId: text('author_id').references(() => users.id),
33 createdAt: timestamp('created_at').defaultNow().notNull(),
34});
35
36// briven
37import { schema, table, text, timestamp } from '@briven/cli/schema';
38export default schema({
39 notes: table({
40 columns: {
41 id: text().primaryKey(),
42 body: text().notNull(),
43 authorId: text().references('users', 'id'),
44 createdAt: timestamp().notNull().default('now()'),
45 },
46 }),
47});`}</Snippet>
48 <p>prisma:</p>
49 <Snippet>{`// prisma
50model Note {
51 id String @id @default(cuid())
52 body String
53 authorId String?
54 author User? @relation(fields: [authorId], references: [id])
55 createdAt DateTime @default(now())
56}
57
58// briven
59notes: table({
60 columns: {
61 id: text().primaryKey(),
62 body: text().notNull(),
63 authorId: text().references('users', 'id'), // FK relation flattens to a column ref
64 createdAt: timestamp().notNull().default('now()'),
65 },
66}),`}</Snippet>
67 <p>conventions to know:</p>
68 <ul className="list-disc pl-5">
69 <li>
70 <strong>column casing.</strong> briven dsl is camelCase in TS; the generated SQL is
71 snake_case-by-convention. if your existing tables already use snake_case, the
72 migration is zero-diff at the SQL layer.
73 </li>
74 <li>
75 <strong>indexes</strong> live on the table&apos;s <code>indexes: [...]</code> array,
76 not chained on the column. compound + unique indexes go here.
77 </li>
78 <li>
79 <strong>generated columns</strong> (drizzle <code>$generated()</code>, postgres{' '}
80 <code>GENERATED ALWAYS AS</code>) aren&apos;t modelled in the dsl yet — declare the
81 column as <code>text()</code>/<code>integer()</code>, then add the GENERATED clause
82 via a custom migration step.
83 </li>
84 </ul>
85 </Section>
86
87 <Section title="query layer — drizzle/prisma → ctx.db">
88 <p>
89 briven&apos;s <code>ctx.db</code> is a focused query builder, not a full ORM. the
90 90% of select / insert / update / delete patterns translate directly:
91 </p>
92 <Snippet>{`// drizzle
93const rows = await db.select().from(notes).where(eq(notes.authorId, id)).orderBy(desc(notes.createdAt)).limit(50);
94
95// briven
96const rows = await ctx.db('notes')
97 .select()
98 .where({ authorId: id })
99 .orderBy('createdAt', 'desc')
100 .limit(50);`}</Snippet>
101 <p>
102 for the remaining 10% — joins, CTEs, window functions, full-text — drop to{' '}
103 <code>ctx.db.execute(sql, params)</code> with a parameterised SQL string. see{' '}
104 <Link href="/functions" className="underline underline-offset-2">/functions</Link> for
105 the full <code>Ctx</code> shape.
106 </p>
107 </Section>
108
109 <Section title="data port — pg_dump | pg_restore">
110 <p>
111 since briven&apos;s data plane is also postgres, the data move is a pg_dump pipe.
112 briven creates a per-project schema (<code>proj_&lt;projectId&gt;</code>); your
113 existing <code>public</code> schema lands inside it.
114 </p>
115 <Snippet>{`# 1. open a short-lived dsn into the briven project's schema
116briven db shell-token > /tmp/briven-dsn # writes a single-line dsn
117
118# 2. dump source, restore into briven, scoped to public
119pg_dump --schema=public --no-owner --no-privileges \\
120 --format=custom \\
121 "$SOURCE_DATABASE_URL" \\
122 | pg_restore --no-owner --no-privileges \\
123 --schema=public \\
124 --dbname="$(cat /tmp/briven-dsn)"
125
126# 3. verify row counts match
127psql "$SOURCE_DATABASE_URL" -tAc 'select count(*) from notes'
128psql "$(cat /tmp/briven-dsn)" -tAc 'select count(*) from public.notes'`}</Snippet>
129 <p>
130 the briven dsn is short-lived (15 minutes per issuance) — issue a fresh one if your
131 dump runs longer.
132 </p>
133 </Section>
134
135 <Section title="functions port — drizzle/prisma handlers → briven functions">
136 <p>
137 your existing API handlers (express, fastify, hono, next.js api routes) become files
138 under <code>briven/functions/</code>. one file per endpoint:
139 </p>
140 <Snippet>{`// before: express + drizzle
141app.get('/api/notes', async (req, res) => {
142 const rows = await db.select().from(notes).where(eq(notes.authorId, req.user.id));
143 res.json({ notes: rows });
144});
145
146// after: briven/functions/getNotes.ts
147import { query, type Ctx } from '@briven/cli/server';
148export default query(async (ctx: Ctx) => {
149 if (!ctx.auth) throw new Error('unauthorized');
150 return await ctx.db('notes').select().where({ authorId: ctx.auth.userId });
151});`}</Snippet>
152 <p>
153 the wrapping framework goes away — briven owns the http surface. invoke from the
154 client via <code>briven invoke getNotes</code>, or via the SDK&apos;s reactive{' '}
155 <code>useQuery</code>.
156 </p>
157 </Section>
158
159 <Section title="auth port">
160 <p>
161 if you were rolling your own auth on top of postgres (sessions table + cookie + bcrypt),
162 Better Auth gives you the same primitives without the maintenance burden. magic-link
163 + email/password + GitHub OAuth ship out of the box; bring your{' '}
164 <code>users.id</code> column over and Better Auth keeps using it.
165 </p>
166 </Section>
167 </DocsShell>
168 );
169}
170
171function Section({ title, children }: { title: string; children: React.ReactNode }) {
172 return (
173 <section className="mt-10">
174 <h2 className="font-mono text-lg">{title}</h2>
175 <div className="mt-2 space-y-3 font-mono text-sm text-[var(--color-text-muted)]">
176 {children}
177 </div>
178 </section>
179 );
180}
181
182function Snippet({ children }: { children: string }) {
183 return (
184 <pre className="overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs">
185 <code>{children}</code>
186 </pre>
187 );
188}