page.tsx177 lines · main
| 1 | import Link from 'next/link'; |
| 2 | |
| 3 | import { DocsShell } from '../../../components/shell'; |
| 4 | |
| 5 | export const metadata = { title: 'migration · supabase → briven' }; |
| 6 | |
| 7 | export 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('...')</code> from <code>@briven/shared</code> rather than a |
| 47 | sequence; this is the briven idiom and avoids the "IDs visible to attackers" |
| 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'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's own role rather than the end-user's. |
| 64 | </p> |
| 65 | <p>port each <code>CREATE POLICY</code> to a guard inside your function:</p> |
| 66 | <Snippet>{`-- supabase |
| 67 | CREATE POLICY "users see own notes" |
| 68 | ON notes FOR SELECT |
| 69 | USING (auth.uid() = author_id); |
| 70 | |
| 71 | // briven/functions/getNotes.ts |
| 72 | import { query, type Ctx } from '@briven/cli/server'; |
| 73 | export 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 |
| 94 | serve(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 |
| 101 | import { mutation, type Ctx } from '@briven/cli/server'; |
| 102 | import { z } from 'zod'; |
| 103 | |
| 104 | const Args = z.object({ email: z.string().email(), role: z.string() }); |
| 105 | |
| 106 | export 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's{' '} |
| 117 | <code>auth.users</code> table doesn't exist on briven — there'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's <code>users.id</code> to |
| 122 | supabase's <code>auth.users.id</code> (it's a uuid; briven'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_<projectId>/<userPath></code> — |
| 131 | your existing bucket can be cp'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's schema: |
| 141 | </p> |
| 142 | <Snippet>{`# dump |
| 143 | pg_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\` |
| 148 | pg_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 | |
| 160 | function 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 | |
| 171 | function 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 | } |