page.tsx177 lines · main
1import Link from 'next/link';
2
3import { DocsShell } from '../../../components/shell';
4
5export const metadata = { title: 'migration · supabase → briven' };
6
7export default function SupabaseMigrationPage() {
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">supabase → briven</h1>
16 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
17 port a supabase 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 documents the supabase-specific parts (RLS, edge functions, auth, storage).
22 </p>
23
24 <div className="mt-6 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]">
25 <strong>good news:</strong> supabase is already postgres. the schema port is mostly
26 copy-paste; only RLS + edge functions + auth need real work.
27 </div>
28
29 <Section title="schema port — postgres → briven dsl">
30 <p>
31 dump your public schema and translate the <code>CREATE TABLE</code> statements
32 one-to-one into the briven dsl. the column types map directly:
33 </p>
34 <ul className="list-disc pl-5">
35 <li>
36 <code>text</code> / <code>varchar(n)</code> / <code>integer</code> /{' '}
37 <code>bigint</code> / <code>boolean</code> / <code>timestamptz</code> /{' '}
38 <code>uuid</code> / <code>jsonb</code> all have direct briven dsl equivalents (see{' '}
39 <Link href="/schema" className="underline underline-offset-2">
40 /schema
41 </Link>
42 ).
43 </li>
44 <li>
45 <code>SERIAL</code> / <code>BIGSERIAL</code> → use a ulid <code>text().primaryKey()</code>{' '}
46 with <code>newId(&apos;...&apos;)</code> from <code>@briven/shared</code> rather than a
47 sequence; this is the briven idiom and avoids the &quot;IDs visible to attackers&quot;
48 class of bugs.
49 </li>
50 <li>
51 postgres enums (<code>CREATE TYPE ...</code>) → <code>text()</code> with
52 application-level validation. the briven dsl doesn&apos;t have a first-class enum
53 yet; the validation pattern stays in your function code.
54 </li>
55 </ul>
56 </Section>
57
58 <Section title="row-level-security policies do NOT carry over">
59 <p>
60 briven enforces tenancy in <strong>function code</strong>, not via postgres RLS. this
61 is a deliberate trade-off — RLS is brittle when you have to reason about which role
62 a query is running as, and the connection-pool model briven uses runs every query
63 as the project&apos;s own role rather than the end-user&apos;s.
64 </p>
65 <p>port each <code>CREATE POLICY</code> to a guard inside your function:</p>
66 <Snippet>{`-- supabase
67CREATE POLICY "users see own notes"
68ON notes FOR SELECT
69USING (auth.uid() = author_id);
70
71// briven/functions/getNotes.ts
72import { query, type Ctx } from '@briven/cli/server';
73export default query(async (ctx: Ctx) => {
74 if (!ctx.auth) throw new Error('unauthorized');
75 return await ctx.db('notes')
76 .select()
77 .where({ authorId: ctx.auth.userId });
78});`}</Snippet>
79 <p>
80 this is more code per query but easier to reason about, easier to log, and easier to
81 test. side benefit: no policy-recompile pause on a schema change.
82 </p>
83 </Section>
84
85 <Section title="edge functions port">
86 <p>
87 supabase edge functions are deno scripts; briven functions are also deno isolates
88 (see <Link href="/functions" className="underline underline-offset-2">/functions</Link>
89 ). the wire format is different (briven functions are invoked via{' '}
90 <code>POST /v1/projects/:id/functions/:name</code>, not over a custom edge runtime),
91 but the handler shape ports cleanly:
92 </p>
93 <Snippet>{`// supabase: supabase/functions/sendInvite/index.ts
94serve(async (req) => {
95 const { email, role } = await req.json();
96 // ... call mittera, write to db ...
97 return new Response(JSON.stringify({ ok: true }));
98});
99
100// briven: briven/functions/sendInvite.ts
101import { mutation, type Ctx } from '@briven/cli/server';
102import { z } from 'zod';
103
104const Args = z.object({ email: z.string().email(), role: z.string() });
105
106export default mutation(async (ctx: Ctx, raw: unknown) => {
107 const { email, role } = Args.parse(raw);
108 // ... call mittera (signing secret in ctx.env), write to db ...
109 return { ok: true };
110});`}</Snippet>
111 </Section>
112
113 <Section title="auth port">
114 <p>
115 supabase auth → Better Auth. briven supports magic-link + email/password + GitHub
116 OAuth out of the box. supabase&apos;s{' '}
117 <code>auth.users</code> table doesn&apos;t exist on briven — there&apos;s a single{' '}
118 <code>users</code> table with email + name + verifiedAt.
119 </p>
120 <p>
121 to preserve user IDs across the cut, set briven&apos;s <code>users.id</code> to
122 supabase&apos;s <code>auth.users.id</code> (it&apos;s a uuid; briven&apos;s text
123 primary key accepts it directly) during the data-import step.
124 </p>
125 </Section>
126
127 <Section title="storage port">
128 <p>
129 supabase storage → MinIO (briven.tech) or any S3-compatible bucket (self-host).
130 the path layout briven uses is <code>p_&lt;projectId&gt;/&lt;userPath&gt;</code> —
131 your existing bucket can be cp&apos;d wholesale into the new namespace.{' '}
132 <code>briven storage</code> as a CLI command lands with the public beta; until then
133 use the AWS or rclone CLIs against the briven minio endpoint.
134 </p>
135 </Section>
136
137 <Section title="data dump → briven">
138 <p>
139 supabase exposes a postgres connection on the dashboard. dump the public schema,
140 restore into the briven project&apos;s schema:
141 </p>
142 <Snippet>{`# dump
143pg_dump --schema=public --no-owner --no-privileges \\
144 --format=custom --file=supabase.dump \\
145 "$SUPABASE_DATABASE_URL"
146
147# restore — connect with the dsn from \`briven db shell-token\`
148pg_restore --no-owner --no-privileges \\
149 --schema=public --dbname="$BRIVEN_PROJECT_DSN" \\
150 supabase.dump
151
152# briven's data plane creates a per-project schema (proj_<id>); the
153# above restore writes into "public" inside that schema. adjust
154# search_path if your queries assume bare table names.`}</Snippet>
155 </Section>
156 </DocsShell>
157 );
158}
159
160function Section({ title, children }: { title: string; children: React.ReactNode }) {
161 return (
162 <section className="mt-10">
163 <h2 className="font-mono text-lg">{title}</h2>
164 <div className="mt-2 space-y-3 font-mono text-sm text-[var(--color-text-muted)]">
165 {children}
166 </div>
167 </section>
168 );
169}
170
171function Snippet({ children }: { children: string }) {
172 return (
173 <pre className="overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs">
174 <code>{children}</code>
175 </pre>
176 );
177}