page.tsx630 lines · main
1import type { Metadata } from 'next';
2import Link from 'next/link';
3import { notFound } from 'next/navigation';
4
5import { BackgroundGrid } from '../../../components/marketing/background-grid';
6import { MigrationLeadForm } from '../../../components/marketing/migration-lead-form';
7import { SiteFooter } from '../../../components/marketing/site-footer';
8import { SiteHeader } from '../../../components/marketing/site-header';
9import { TrackPageView } from '../../../components/marketing/track-page-view';
10import { getSessionUser } from '../../../lib/session';
11
12interface SourceDetail {
13 slug: string;
14 name: string;
15 hero: string;
16 whyLeave: string;
17 // Three reassurance bullets — what the customer is afraid of, named
18 // explicitly and resolved.
19 fears: readonly { title: string; body: string }[];
20 // The conceptual mapping in their language → briven's.
21 mappings: readonly { from: string; to: string }[];
22 // Honest, ranked: what comes for free, what we automate, what's manual.
23 effortLines: readonly { effort: 'free' | 'auto' | 'manual'; body: string }[];
24 // FAQ — answers questions someone evaluating the migration actually has.
25 faq: readonly { q: string; a: string }[];
26}
27
28// Marketing per-source pages. Read at request time so we can ship one
29// page file and nine SourceDetail entries instead of nine route files.
30const SOURCES: Record<string, SourceDetail> = {
31 convex: {
32 slug: 'convex',
33 name: 'convex',
34 hero: 'your reactive backend, on plain postgres.',
35 whyLeave:
36 'convex pioneered the reactive-queries pattern briven adopts. the difference is the floor: convex stores your data in its own engine; briven stores it in plain postgres, which means pg_dump moves your whole product anywhere — convex, supabase, a self-hosted server, anywhere. same reactivity, no proprietary database.',
37 fears: [
38 {
39 title: 'will my useQuery hooks change?',
40 body: "no — briven's useQuery() matches convex 1:1. swap the import to @briven/client-react and your react tree compiles unchanged.",
41 },
42 {
43 title: 'what happens to my data during the move?',
44 body: "we copy it. your convex deployment stays running and untouched. nothing on convex is deleted or modified — we read, we write a copy into briven, you keep both running in parallel until you flip writes.",
45 },
46 {
47 title: 'how long will i be in a weird half-migrated state?',
48 body: 'as long as you want. parallel-run is mandatory; cutover is your decision. we keep convex warm for 7 days after cutover as a one-click rollback target.',
49 },
50 ],
51 mappings: [
52 { from: 'defineTable() with v.string() / v.number() / v.id()', to: 'briven schema DSL: text(), bigint(), text().references()' },
53 { from: 'query() / mutation() / action() (multiple per file)', to: 'one default export per file under briven/functions/' },
54 { from: 'v.id("users") → fk to users table', to: 'text().references("users", "id")' },
55 { from: 'ctx.db.query("notes").withIndex(...).collect()', to: 'ctx.db("notes").select().where({ ... }).orderBy(...)' },
56 { from: 'useQuery("getNotes", args) on the client', to: 'useQuery("getNotes", args) — same signature' },
57 { from: 'clerk / auth0 / convex auth', to: 'Better Auth (email + magic-link + OAuth)' },
58 ],
59 effortLines: [
60 { effort: 'free', body: 'data export — `npx convex export` already dumps every table to one zip; we read it directly.' },
61 { effort: 'auto', body: 'schema translation — your TS schema in convex/schema.ts ports to briven schema DSL via our walker.' },
62 { effort: 'auto', body: 'function translation — query / mutation / action handlers port to briven/functions/*.ts; the 80% pattern-matchable cases are translated automatically with diffs you review.' },
63 { effort: 'manual', body: 'scheduler primitives — `ctx.scheduler.runAfter(...)` doesn’t exist in briven yet. we replace with a pg_cron entry or an action-driven sleep-and-poll.' },
64 { effort: 'manual', body: 'auth cutover — convex auth doesn’t translate. we either preserve user ids during data import (so foreign keys still resolve) or force a one-time sign-in. you pick.' },
65 ],
66 faq: [
67 { q: 'how much downtime?', a: 'zero. your convex deployment serves traffic the entire migration. cutover is a DNS / client config flip you control. parallel-run lasts as long as you want.' },
68 { q: 'what about my custom server-side functions?', a: 'we port each one and flag the cases we can’t auto-translate (custom 3rd-party SDKs, ctx.scheduler, ctx.storage). you review every translated file before it ships.' },
69 { q: 'can i go back to convex?', a: 'yes, for 7 days after cutover. we keep your convex deployment in read-mirror sync with briven during that window. if you press rollback, your customers see no break.' },
70 { q: 'is briven really cheaper than convex?', a: 'roughly. compare pricing at /pricing — briven\'s free tier is more generous for small projects, and the paid tier scales linearly on storage + invocations instead of convex\'s opaque bundling.' },
71 ],
72 },
73
74 supabase: {
75 slug: 'supabase',
76 name: 'supabase',
77 hero: 'supabase + convex-style reactivity, on the same postgres.',
78 whyLeave:
79 'supabase exposes postgres directly through postgrest + row-level security; briven puts a typed function layer in front and adds convex-style reactive subscriptions. you keep the postgres you already trust, with proper functions and reactive queries on top.',
80 fears: [
81 {
82 title: 'will i lose my data?',
83 body: "no. it's postgres on both ends. pg_dump | pg_restore moves every row to briven without translation. row-counts match exactly.",
84 },
85 {
86 title: 'what happens to my row-level security policies?',
87 body: 'they become explicit guards inside your briven functions. instead of `auth.uid() = user_id` in SQL, you write `if (ctx.session.userId !== row.userId) throw forbidden`. you can read the rule in code.',
88 },
89 {
90 title: 'do my edge functions port?',
91 body: 'directly. supabase edge functions (Deno) port to briven functions (also Deno-isolate based). the request/response signature is the same shape.',
92 },
93 ],
94 mappings: [
95 { from: 'supabase create table', to: 'briven schema DSL → identical postgres tables' },
96 { from: 'row-level security policy', to: 'explicit guard in the briven function (readable, debuggable)' },
97 { from: 'supabase edge functions (Deno)', to: 'briven functions (Deno isolates) — same runtime, same imports' },
98 { from: 'supabase auth (GoTrue)', to: 'Better Auth — email + magic-link + OAuth. user ids preserved.' },
99 { from: 'supabase realtime (postgres replication)', to: 'briven realtime (LISTEN/NOTIFY + WS). useQuery() hook.' },
100 { from: 'supabase storage', to: 'briven storage (S3-compatible MinIO under the hood)' },
101 ],
102 effortLines: [
103 { effort: 'free', body: 'data move — pg_dump from supabase, pg_restore into briven\'s data plane. no transformation.' },
104 { effort: 'auto', body: 'schema port — the schema is already postgres; we just bring it across.' },
105 { effort: 'auto', body: 'edge functions — port directly. same Deno-isolate runtime, same Web standard APIs.' },
106 { effort: 'manual', body: 'RLS → function guards — every policy gets rewritten as an explicit check inside the function that replaces the affected query. tedious; mechanical.' },
107 { effort: 'manual', body: 'supabase auth → Better Auth — user table copies cleanly. OAuth providers reconfigure. one-time fresh sign-in for password users.' },
108 ],
109 faq: [
110 { q: 'will my client-side @supabase/supabase-js calls still work?', a: 'no — you swap that client for @briven/client-react. but the queries you used to write inline as postgrest filters become typed briven functions instead, which is a clarity upgrade.' },
111 { q: 'what about my supabase storage buckets?', a: 'briven storage is S3-compatible (MinIO). we mirror your buckets one-to-one and rewrite your client-side upload URLs.' },
112 { q: 'are realtime subscriptions faster on briven?', a: 'comparable. briven uses LISTEN/NOTIFY where supabase uses logical replication. both deliver <100ms updates for typical loads.' },
113 { q: 'what about pgvector / postgis / other extensions?', a: 'briven\'s postgres is the same postgres — extensions you used on supabase work on briven if your tier permits. pgvector is enabled by default.' },
114 ],
115 },
116
117 firebase: {
118 slug: 'firebase',
119 name: 'firebase / firestore',
120 hero: 'firestore in your head, postgres on the floor.',
121 whyLeave:
122 'firebase is nosql with realtime built-in. briven is real sql on postgres with realtime built-in. the hardest part of moving off firebase is the modelling — document → relational requires shape decisions per collection. we walk through them with you. we don\'t just dump.',
123 fears: [
124 {
125 title: 'do i have to redesign my data model?',
126 body: 'some of it, yes — but we don\'t make you do it alone. briven samples your documents per collection and proposes a relational schema (flatten vs jsonb per field). you review every collection before we commit.',
127 },
128 {
129 title: 'what about my realtime listeners?',
130 body: 'they become briven useQuery() hooks. the callback-based snapshot model maps cleanly to react hooks; the wire model is different but the developer ergonomics are familiar.',
131 },
132 {
133 title: 'do my users keep their accounts?',
134 body: 'yes. firebase auth users (email/password + OAuth) port to Better Auth. we preserve provider+subject pairs so a "sign in with Google" user lands on their existing account, no fresh signup.',
135 },
136 ],
137 mappings: [
138 { from: 'firestore collection', to: 'postgres table with shape decisions per field' },
139 { from: 'embedded subdocument', to: 'jsonb column OR child table (your choice during review)' },
140 { from: 'firestore document id', to: 'text column with ULID for new rows' },
141 { from: 'firebase auth (Google/Apple/email)', to: 'Better Auth — providers reconfigure 1:1' },
142 { from: 'realtime onSnapshot listener', to: 'useQuery() — react hook, same wire-level guarantees' },
143 { from: 'firebase storage', to: 'briven storage (S3-compatible, MinIO)' },
144 { from: 'firebase functions (gen 2)', to: 'briven functions (Deno isolates)' },
145 ],
146 effortLines: [
147 { effort: 'auto', body: 'data export — firebase admin SDK dumps every collection. we stream it.' },
148 { effort: 'auto', body: 'schema proposal — we sample your documents and propose a postgres schema. you review per collection before we commit.' },
149 { effort: 'auto', body: 'auth — firebase auth users map to Better Auth, preserving provider+subject. existing OAuth users land on their account on first sign-in.' },
150 { effort: 'manual', body: 'shape decisions — for each collection, you decide: flatten into columns? jsonb blob? hybrid? we propose, you approve.' },
151 { effort: 'manual', body: 'cloud functions — port to briven functions; gen-2 Deno-style functions are close, but firebase-specific SDKs become postgres queries.' },
152 { effort: 'manual', body: 'security rules → function guards — every rule becomes an explicit check inside the briven function that replaces the firestore access.' },
153 ],
154 faq: [
155 { q: 'isn\'t document → relational just downgrading?', a: 'no — briven supports jsonb columns, which give you document-store flexibility *inside* a relational schema. you can keep nested docs as opaque jsonb and only flatten the fields you query on. best of both.' },
156 { q: 'how long does a firebase migration take?', a: 'longer than the others. we recommend a 2-week parallel-run window for non-trivial firebase apps because shape decisions surface over time. quoted as ~3-5 days of operator time.' },
157 { q: 'what about firebase\'s offline persistence?', a: 'briven\'s client-side cache covers offline reads. offline writes require app-level queueing — same as firebase, just less invisible.' },
158 { q: 'will my flutter / unity / SwiftUI app work?', a: 'briven has REST + WS endpoints that any client can hit. official SDKs are TS-only today; native clients ship over the next two quarters.' },
159 ],
160 },
161
162 mongodb: {
163 slug: 'mongodb',
164 name: 'mongodb',
165 hero: 'document flexibility, postgres durability.',
166 whyLeave:
167 'mongodb gave you schema flexibility; briven\'s jsonb columns + postgres tables give you the same thing without the operational surface of a separate database. and you get convex-style reactivity for free on top.',
168 fears: [
169 {
170 title: 'do i have to flatten every nested document?',
171 body: 'no — fields you don\'t query on stay as jsonb. the rule we apply: if you ever filter or sort on a field, flatten it into a column. otherwise leave it in jsonb. you review per collection.',
172 },
173 {
174 title: 'what about ObjectId, my embedded refs, $lookup pipelines?',
175 body: 'ObjectIds become text columns with ULIDs for new rows. embedded refs become foreign keys. $lookup pipelines become postgres joins, which are faster.',
176 },
177 {
178 title: 'can i still use aggregation-style queries?',
179 body: 'yes — postgres has window functions, CTEs, and json operators. anything mongo aggregation does, postgres does too. usually faster.',
180 },
181 ],
182 mappings: [
183 { from: 'collection', to: 'table (with jsonb columns where appropriate)' },
184 { from: 'ObjectId', to: 'text + ULID for new rows; preserved for existing rows' },
185 { from: 'embedded document', to: 'jsonb column OR child table (decide per field)' },
186 { from: '$lookup aggregation', to: 'JOIN in a briven function (faster, indexable)' },
187 { from: 'mongoose schema validation', to: 'zod validation inside the briven function' },
188 { from: 'change streams', to: 'useQuery() reactivity on touched tables' },
189 ],
190 effortLines: [
191 { effort: 'auto', body: 'data export — mongoexport per collection, streamed.' },
192 { effort: 'auto', body: 'schema proposal — we sample documents and propose a relational schema. you decide per field: flatten or jsonb.' },
193 { effort: 'manual', body: 'shape decisions — same as firebase. document-store data needs human review to land well in postgres.' },
194 { effort: 'manual', body: 'application-side queries — mongoose / native driver calls become ctx.db chains. mechanical but tedious.' },
195 ],
196 faq: [
197 { q: 'won\'t i lose flexibility?', a: 'only where it was hurting you. jsonb columns preserve schemaless storage for the fields where flexibility helped; flattened columns add indexes + foreign keys where you wanted them.' },
198 { q: 'what about atlas vector search?', a: 'briven has pgvector enabled by default. vector search works on the same postgres; you don\'t need a separate vector store.' },
199 { q: 'how does the move compare to migrating to documentdb / cosmos?', a: 'simpler. those are still document stores with mongo-compatible APIs; briven is a fundamentally different data model. but the migration is more thoughtful — you exit nosql, you don\'t just sideways-port.' },
200 ],
201 },
202
203 drizzle: {
204 slug: 'drizzle',
205 name: 'drizzle',
206 hero: 'same postgres. same TS schema. plus reactivity, functions, hosting.',
207 whyLeave:
208 'drizzle is a great query builder. briven is drizzle\'s shape + a hosted database + typed functions + reactive subscriptions + a deployment pipeline. you keep almost everything you already wrote.',
209 fears: [
210 {
211 title: 'is briven\'s schema DSL different from drizzle\'s?',
212 body: 'minimally. both target postgres. column types map 1:1 (text, integer, boolean, timestamp, jsonb, etc.). drizzle\'s `pgTable()` becomes briven\'s `table()`. relations and indexes carry over.',
213 },
214 {
215 title: 'do i lose my drizzle migrations?',
216 body: 'no — we pg_dump your data from whatever postgres drizzle was pointing at. your existing schema lands intact. briven owns future migrations going forward.',
217 },
218 {
219 title: 'will my db calls still be type-safe?',
220 body: 'yes. briven\'s ctx.db is fully typed against your schema. inferred row types, inferred where-clause shapes, etc. — drizzle\'s big strength carries.',
221 },
222 ],
223 mappings: [
224 { from: 'drizzle pgTable("notes", { ... })', to: 'briven table({ ... }) — same column builders' },
225 { from: 'drizzle relations', to: 'briven foreign keys + JOIN helpers' },
226 { from: 'db.select().from(notes).where(eq(notes.id, x))', to: 'ctx.db("notes").select().where({ id: x })' },
227 { from: 'your own pg-pool deployment', to: 'briven managed postgres + connection pooling' },
228 { from: 'manual migration files', to: 'briven\'s schema diff → auto-generated migrations on deploy' },
229 ],
230 effortLines: [
231 { effort: 'free', body: 'data — your postgres becomes briven\'s postgres via pg_dump | pg_restore.' },
232 { effort: 'auto', body: 'schema translation — drizzle schema.ts ports almost line-for-line to briven\'s DSL.' },
233 { effort: 'manual', body: 'service-layer port — the code that calls drizzle becomes briven function files. one file per function. usually ~1 day for a medium app.' },
234 ],
235 faq: [
236 { q: 'why move at all if i already have a working drizzle setup?', a: 'three reasons: (1) hosted postgres + connection pooling without ops, (2) reactive subscriptions via useQuery, (3) zero-config function deployment. if you\'re happy running pg yourself, you don\'t need briven.' },
237 { q: 'can i keep drizzle alongside briven?', a: 'briven\'s ctx.db has the same shape but isn\'t literally drizzle. you swap one for the other in the function layer. you can absolutely use drizzle on a separate database (e.g., analytics) and briven for your reactive app.' },
238 { q: 'what about drizzle studio?', a: 'briven studio at /dashboard/projects/{id}/studio is the same idea — table viewer, query runner, schema editor.' },
239 ],
240 },
241
242 prisma: {
243 slug: 'prisma',
244 name: 'prisma',
245 hero: 'leave prisma, keep postgres.',
246 whyLeave:
247 'prisma\'s schema.prisma format and client are powerful but lock you into prisma\'s migration model + their query engine. briven is the same postgres, with a typed query builder that doesn\'t need a code-generation step and ships with realtime + hosting.',
248 fears: [
249 {
250 title: 'do i have to rewrite every PrismaClient call?',
251 body: 'yes — but the new shape is similar. `prisma.notes.findMany({ where: { id } })` becomes `ctx.db("notes").select().where({ id })`. it\'s a find-and-replace exercise, not a redesign.',
252 },
253 {
254 title: 'does my schema.prisma port automatically?',
255 body: 'we parse schema.prisma and emit briven/schema.ts. field decorators map to briven column helpers. relations carry over. enums and check constraints translate.',
256 },
257 {
258 title: 'do i lose prisma\'s migration history?',
259 body: 'we keep your current schema state (that\'s what gets pg_dumped); briven owns future migrations. your old prisma migration files become reference docs, not active.',
260 },
261 ],
262 mappings: [
263 { from: 'schema.prisma model { ... }', to: 'briven table({ ... })' },
264 { from: '@id @default(cuid())', to: 'text().primaryKey() + ULID at insert time' },
265 { from: 'prisma relations (@relation)', to: 'briven foreign keys + JOIN helpers' },
266 { from: 'PrismaClient.user.findMany({ where: ... })', to: 'ctx.db("user").select().where({ ... })' },
267 { from: 'prisma generate (codegen)', to: 'no codegen — briven\'s ctx.db is inferred from your schema' },
268 { from: 'prisma migrate dev', to: 'briven deploy — auto-diffs and generates the migration' },
269 ],
270 effortLines: [
271 { effort: 'free', body: 'data — pg_dump from your prisma database, pg_restore into briven.' },
272 { effort: 'auto', body: 'schema.prisma → briven/schema.ts translator. ~95% of fields are mechanical.' },
273 { effort: 'manual', body: 'PrismaClient → ctx.db rewrite throughout your service layer. find-and-replace plus light editing.' },
274 { effort: 'manual', body: 'prisma middleware → briven function middleware (auth checks, logging, etc.).' },
275 ],
276 faq: [
277 { q: 'will i miss the prisma query engine?', a: 'briven\'s ctx.db is a thin typed wrapper over node-postgres / bun-sql. there\'s no separate query engine binary. for most apps this is faster and simpler.' },
278 { q: 'what about prisma\'s soft-delete and audit extensions?', a: 'you\'ll write these as briven functions instead. it\'s less magical (no monkey-patching) and easier to debug.' },
279 { q: 'does briven generate types from the schema?', a: 'yes, but inline at usage time — no separate codegen step. add a column, the types update.' },
280 ],
281 },
282
283 postgres: {
284 slug: 'postgres',
285 name: 'raw postgres',
286 hero: 'add reactivity, typed functions, and hosting to the postgres you already run.',
287 whyLeave:
288 'if you\'re running raw postgres + a hand-rolled service layer, briven is the smallest possible upgrade: same database, same SQL, but with a typed function layer, reactive subscriptions, and zero-config deploys on top.',
289 fears: [
290 {
291 title: 'will my schema change?',
292 body: 'no. we pg_dump your tables exactly as they are and pg_restore them into briven\'s data plane. column types, constraints, indexes — all preserved.',
293 },
294 {
295 title: 'do i have to give up writing raw SQL?',
296 body: 'no — briven\'s ctx.db.execute("...") is the SQL escape hatch. the typed builder is for the common case; raw SQL is one method call away.',
297 },
298 {
299 title: 'what about my pg_cron / pg_partman / pg_stat_statements extensions?',
300 body: 'briven\'s postgres is the same postgres. pgvector + pg_cron are enabled by default; we can enable additional extensions on your project tier.',
301 },
302 ],
303 mappings: [
304 { from: 'your schema.sql', to: 'briven schema DSL via introspection (one-shot script)' },
305 { from: 'your service layer (express / fastify / etc.)', to: 'briven functions (one file per endpoint)' },
306 { from: 'pgbouncer + connection pooling', to: 'briven\'s built-in pooling' },
307 { from: 'your existing backup pipeline', to: 'briven\'s automated backups + your existing pg_dump pipeline still works' },
308 ],
309 effortLines: [
310 { effort: 'free', body: 'data — pg_dump | pg_restore. zero transformation.' },
311 { effort: 'auto', body: 'schema port — introspection script reads your information_schema and emits briven DSL.' },
312 { effort: 'manual', body: 'service-layer port — your http handlers become briven functions. usually ~1 day for a medium app.' },
313 ],
314 faq: [
315 { q: 'why not just keep my postgres setup?', a: 'you can. briven adds reactivity, typed deploys, and hosting; if you\'re happy with what you have, the only thing you\'re missing is reactive useQuery.' },
316 { q: 'is briven\'s postgres different in any way?', a: 'postgres 17 with pgvector + pg_cron enabled. otherwise identical. you can `pg_dump` from briven to anywhere any day.' },
317 { q: 'what about replication / read replicas?', a: 'on the team tier. brivens managed read replicas come online without a config change.' },
318 ],
319 },
320
321 hasura: {
322 slug: 'hasura',
323 name: 'hasura',
324 hero: 'the database moves for free. the work is the permissions port.',
325 whyLeave:
326 'hasura\'s GraphQL-over-postgres model is powerful but its metadata format makes the permission system hard to audit. briven exposes typed functions instead — every (role, table, action) rule becomes a readable check inside the function that replaces it.',
327 fears: [
328 {
329 title: 'do i lose GraphQL?',
330 body: 'as a wire protocol, yes — briven uses typed RPC over HTTP. for client developers, the ergonomics are similar (typed args, typed return). but the permission story is much clearer.',
331 },
332 {
333 title: 'every action and event trigger... do they port?',
334 body: 'yes. hasura actions become briven mutation functions; event triggers become outbound webhooks (briven ships with HMAC-signed webhook delivery built in).',
335 },
336 {
337 title: 'how do i find every permission rule?',
338 body: 'hasura\'s metadata.yaml lists every (role, table, action) triple. we walk it programmatically and emit one TypeScript guard per rule. you review.',
339 },
340 ],
341 mappings: [
342 { from: 'hasura tracked table', to: 'briven schema DSL (postgres carries directly)' },
343 { from: 'metadata permission rule', to: 'explicit guard inside the briven function (readable, debuggable)' },
344 { from: 'hasura action (webhook)', to: 'briven mutation function' },
345 { from: 'hasura event trigger', to: 'briven outbound webhook subscriber (HMAC-signed)' },
346 { from: 'remote schema', to: 'briven function that calls the remote API' },
347 ],
348 effortLines: [
349 { effort: 'free', body: 'data — pg_dump from your hasura postgres, pg_restore into briven.' },
350 { effort: 'auto', body: 'schema introspection — your existing postgres tables port to briven DSL.' },
351 { effort: 'auto', body: 'event triggers → outbound webhooks — mechanical translation of metadata.' },
352 { effort: 'manual', body: 'permissions port — every (role, table, action) rule becomes a TypeScript guard. tedious but readable. usually ~half a day per 50 rules.' },
353 { effort: 'manual', body: 'client query port — GraphQL queries become typed briven function calls. shape-similar; rewrite is mechanical.' },
354 ],
355 faq: [
356 { q: 'will my dashboards lose GraphQL introspection?', a: 'yes — briven doesn\'t do GraphQL. internal tools that depend on GraphQL introspection need rewiring to the briven HTTP API. mostly 1-day work.' },
357 { q: 'can i keep hasura running for a while?', a: 'yes. parallel-run as long as you want. hasura points at the same postgres, briven points at briven\'s postgres copy; reads from either, writes to both during the window.' },
358 { q: 'what about hasura cloud auto-scaling?', a: 'briven scales horizontally on the team tier. same auto-scaling story, no GraphQL-engine-specific knobs.' },
359 ],
360 },
361
362 nextauth: {
363 slug: 'nextauth',
364 name: 'nextauth / auth.js',
365 hero: 'same user table. same providers. typed functions + hosting on top.',
366 whyLeave:
367 'nextauth.js is auth-only — you still need a database, a service layer, and hosting. briven gives you all three, and because both ship Better Auth\'s table shape, the user migration is the simplest part.',
368 fears: [
369 {
370 title: 'will my users have to sign in again?',
371 body: 'usually no. briven uses Better Auth, and your nextauth users / accounts / sessions tables already match Better Auth\'s schema. we copy the rows; OAuth users land on their account on next sign-in without any action.',
372 },
373 {
374 title: 'do my OAuth providers still work?',
375 body: 'yes. configure the same Google / GitHub / Apple / etc. client IDs in briven, and existing provider+subject pairs resolve to existing accounts.',
376 },
377 {
378 title: 'what about my getServerSession / useSession code?',
379 body: 'replaced with briven\'s session helpers. shape is similar; types are stricter. light find-and-replace.',
380 },
381 ],
382 mappings: [
383 { from: 'nextauth users table', to: 'briven users table (same Better Auth shape)' },
384 { from: 'nextauth accounts table', to: 'briven accounts table (1:1)' },
385 { from: 'nextauth sessions table', to: 'briven sessions (1:1)' },
386 { from: 'getServerSession(authOptions)', to: 'briven session helpers (server components: requireUser())' },
387 { from: 'useSession() hook', to: 'briven\'s useSession (same shape)' },
388 { from: 'authOptions.providers config', to: 'briven dashboard → providers (UI configuration)' },
389 ],
390 effortLines: [
391 { effort: 'free', body: 'data — user/account/session tables copy directly. zero transformation.' },
392 { effort: 'auto', body: 'provider config — point briven at your existing OAuth client IDs.' },
393 { effort: 'manual', body: 'callsite migration — getServerSession + useSession references rewritten throughout your app.' },
394 ],
395 faq: [
396 { q: 'will all my users keep their sessions?', a: 'yes if we copy the sessions table; everyone stays signed in. or we can force one fresh sign-in if you prefer cleaner state.' },
397 { q: 'do i lose nextauth\'s middleware patterns?', a: 'briven uses route-level helpers (requireUser, requireSession). the patterns are similar; cleaner, less middleware-magic.' },
398 { q: 'what about my custom adapter / database?', a: 'briven owns the database — you don\'t bring one. your custom adapter logic becomes briven function code.' },
399 ],
400 },
401};
402
403export function generateMetadata({
404 params,
405}: {
406 params: Promise<{ source: string }>;
407}): Promise<Metadata> {
408 return params.then(({ source }) => {
409 const s = SOURCES[source];
410 if (!s) return { title: 'migrate to briven' };
411 return {
412 title: `migrate ${s.name} to briven — ${s.hero}`,
413 description: s.whyLeave.slice(0, 160),
414 };
415 });
416}
417
418export function generateStaticParams(): { source: string }[] {
419 return Object.keys(SOURCES).map((source) => ({ source }));
420}
421
422export default async function MigrateSourceMarketingPage({
423 params,
424}: {
425 params: Promise<{ source: string }>;
426}) {
427 const { source } = await params;
428 const detail = SOURCES[source];
429 if (!detail) notFound();
430 const user = await getSessionUser().catch(() => null);
431 const startHref = user
432 ? `/dashboard/projects/new/migrate/${detail.slug}`
433 : `/signin?next=/dashboard/projects/new/migrate/${detail.slug}`;
434
435 return (
436 <main className="relative min-h-dvh overflow-hidden bg-[var(--color-bg)] text-[var(--color-text)]">
437 <TrackPageView
438 apiOrigin={process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN ?? ''}
439 source={detail.slug}
440 />
441 <BackgroundGrid />
442 <SiteHeader user={user} />
443
444 <section className="relative z-10 mx-auto w-full max-w-4xl px-6 pb-12 pt-16 sm:pt-24">
445 <p className="font-mono uppercase tracking-[0.12em] text-[var(--color-primary)] text-[var(--text-xs)]">
446 migrate / {detail.slug}
447 </p>
448 <h1 className="mt-4 font-sans font-medium leading-[1.05] tracking-[-0.03em] text-[var(--color-text)] text-[var(--text-display-3)] sm:text-[var(--text-display-2)]">
449 {detail.hero}
450 </h1>
451 <p className="mt-6 max-w-2xl leading-[1.6] text-[var(--color-text-muted)] text-[var(--text-body)]">
452 {detail.whyLeave}
453 </p>
454 <div className="mt-8 flex flex-wrap items-center gap-3">
455 <Link
456 href={startHref}
457 className="inline-flex h-12 items-center justify-center rounded-[var(--radius-md)] bg-[var(--color-primary)] px-6 font-sans font-medium text-[var(--color-text-inverse)] shadow-[var(--shadow-sm)] transition-colors duration-[var(--duration-fast)] ease-[var(--ease-briven)] hover:bg-[var(--color-primary-hover)] active:bg-[var(--color-primary-pressed)]"
458 >
459 start the {detail.name} migration
460 </Link>
461 <a
462 href={`mailto:migrations@flndrn.com?subject=${encodeURIComponent(`${detail.name} → briven migration`)}`}
463 className="inline-flex h-12 items-center justify-center rounded-[var(--radius-md)] border border-[var(--color-border)] bg-transparent px-6 font-sans font-medium text-[var(--color-text)] transition-colors duration-[var(--duration-fast)] ease-[var(--ease-briven)] hover:border-[var(--color-border-strong)] hover:bg-[var(--color-surface-raised)]"
464 >
465 email a human first
466 </a>
467 </div>
468 </section>
469
470 <section className="relative z-10 mx-auto w-full max-w-4xl px-6 pb-16">
471 <h2 className="font-mono uppercase tracking-[0.12em] text-[var(--color-text-subtle)] text-[var(--text-xs)]">
472 what you&apos;re afraid of
473 </h2>
474 <div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-3">
475 {detail.fears.map((f) => (
476 <div
477 key={f.title}
478 className="flex flex-col gap-2 rounded-[var(--radius-lg)] border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-5"
479 >
480 <p className="font-mono text-sm text-[var(--color-text)]">{f.title}</p>
481 <p className="leading-[1.6] text-[var(--color-text-muted)] text-[var(--text-small)]">
482 {f.body}
483 </p>
484 </div>
485 ))}
486 </div>
487 </section>
488
489 <section className="relative z-10 mx-auto w-full max-w-4xl px-6 pb-16">
490 <h2 className="font-mono uppercase tracking-[0.12em] text-[var(--color-text-subtle)] text-[var(--text-xs)]">
491 the conceptual mapping
492 </h2>
493 <p className="mt-2 leading-[1.6] text-[var(--color-text-muted)] text-[var(--text-small)]">
494 your mental model survives the move. left column: what you call it in {detail.name}.
495 right column: where it lives in briven.
496 </p>
497 <div className="mt-6 overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-border-subtle)]">
498 <table className="w-full font-mono text-xs">
499 <thead className="bg-[var(--color-surface)]">
500 <tr>
501 <th className="px-4 py-3 text-left text-[var(--color-text-subtle)] uppercase tracking-wider">
502 {detail.name}
503 </th>
504 <th className="px-4 py-3 text-left text-[var(--color-text-subtle)] uppercase tracking-wider">
505 briven
506 </th>
507 </tr>
508 </thead>
509 <tbody>
510 {detail.mappings.map((m, i) => (
511 <tr
512 key={i}
513 className="border-t border-[var(--color-border-subtle)] text-[var(--color-text-muted)]"
514 >
515 <td className="px-4 py-3 align-top">{m.from}</td>
516 <td className="px-4 py-3 align-top text-[var(--color-text)]">{m.to}</td>
517 </tr>
518 ))}
519 </tbody>
520 </table>
521 </div>
522 </section>
523
524 <section className="relative z-10 mx-auto w-full max-w-4xl px-6 pb-16">
525 <h2 className="font-mono uppercase tracking-[0.12em] text-[var(--color-text-subtle)] text-[var(--text-xs)]">
526 what the migration actually costs
527 </h2>
528 <p className="mt-2 leading-[1.6] text-[var(--color-text-muted)] text-[var(--text-small)]">
529 ranked honestly: what comes for free, what we automate, what stays manual.
530 </p>
531 <ul className="mt-6 flex flex-col gap-3">
532 {detail.effortLines.map((e, i) => (
533 <li
534 key={i}
535 className="flex items-start gap-3 rounded-[var(--radius-lg)] border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4"
536 >
537 <span
538 className={`rounded-full border px-2 py-0.5 font-mono text-[10px] uppercase tracking-wider ${effortTone(e.effort)}`}
539 >
540 {effortLabel(e.effort)}
541 </span>
542 <p className="flex-1 leading-[1.6] text-[var(--color-text-muted)] text-[var(--text-small)]">
543 {e.body}
544 </p>
545 </li>
546 ))}
547 </ul>
548 </section>
549
550 <section className="relative z-10 mx-auto w-full max-w-4xl px-6 pb-16">
551 <h2 className="font-mono uppercase tracking-[0.12em] text-[var(--color-text-subtle)] text-[var(--text-xs)]">
552 questions you actually have
553 </h2>
554 <dl className="mt-4 flex flex-col gap-4">
555 {detail.faq.map((item, i) => (
556 <div
557 key={i}
558 className="rounded-[var(--radius-lg)] border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-5"
559 >
560 <dt className="font-mono text-sm text-[var(--color-text)]">{item.q}</dt>
561 <dd className="mt-2 leading-[1.6] text-[var(--color-text-muted)] text-[var(--text-small)]">
562 {item.a}
563 </dd>
564 </div>
565 ))}
566 </dl>
567 </section>
568
569 <section className="relative z-10 mx-auto w-full max-w-4xl px-6 pb-16">
570 <h2 className="font-mono uppercase tracking-[0.12em] text-[var(--color-text-subtle)] text-[var(--text-xs)]">
571 ready to start? no signup needed
572 </h2>
573 <p className="mt-2 leading-[1.6] text-[var(--color-text-muted)] text-[var(--text-small)]">
574 leave us your email and we&apos;ll reach out within one business day. your{' '}
575 {detail.name} stays untouched the entire time.
576 </p>
577 <div className="mt-6">
578 <MigrationLeadForm
579 apiOrigin={process.env.NEXT_PUBLIC_BRIVEN_API_ORIGIN ?? ''}
580 defaultSource={detail.slug}
581 sources={Object.values(SOURCES).map((s) => ({ slug: s.slug, name: s.name }))}
582 />
583 </div>
584 </section>
585
586 <section className="relative z-10 mx-auto w-full max-w-4xl px-6 pb-24">
587 <div className="rounded-[var(--radius-lg)] border border-[var(--color-border-primary)] bg-[var(--color-primary-subtle)] p-6">
588 <h2 className="font-sans font-medium tracking-[-0.02em] text-[var(--color-text)] text-[var(--text-h3)]">
589 already got a briven account?
590 </h2>
591 <p className="mt-3 leading-[1.6] text-[var(--color-text-muted)] text-[var(--text-body)]">
592 jump straight into the in-product wizard for a more detailed intake form — you
593 can save progress and track status from your dashboard.
594 </p>
595 <div className="mt-5 flex flex-wrap gap-3">
596 <Link
597 href={startHref}
598 className="inline-flex h-11 items-center justify-center rounded-[var(--radius-md)] bg-[var(--color-primary)] px-5 font-sans font-medium text-[var(--color-text-inverse)] hover:bg-[var(--color-primary-hover)]"
599 >
600 open the dashboard wizard
601 </Link>
602 <Link
603 href="/migrate"
604 className="inline-flex h-11 items-center justify-center rounded-[var(--radius-md)] border border-[var(--color-border)] px-5 font-sans font-medium text-[var(--color-text)] hover:border-[var(--color-border-strong)]"
605 >
606 compare all sources
607 </Link>
608 </div>
609 </div>
610 </section>
611
612 <SiteFooter />
613 </main>
614 );
615}
616
617function effortLabel(effort: 'free' | 'auto' | 'manual'): string {
618 return effort;
619}
620
621function effortTone(effort: 'free' | 'auto' | 'manual'): string {
622 switch (effort) {
623 case 'free':
624 return 'border-[var(--color-primary)] text-[var(--color-primary)] bg-[var(--color-primary-subtle)]';
625 case 'auto':
626 return 'border-[var(--color-border-primary)] text-[var(--color-text)]';
627 case 'manual':
628 return 'border-[var(--color-warning)] text-[var(--color-warning)]';
629 }
630}