page.tsx630 lines · main
| 1 | import type { Metadata } from 'next'; |
| 2 | import Link from 'next/link'; |
| 3 | import { notFound } from 'next/navigation'; |
| 4 | |
| 5 | import { BackgroundGrid } from '../../../components/marketing/background-grid'; |
| 6 | import { MigrationLeadForm } from '../../../components/marketing/migration-lead-form'; |
| 7 | import { SiteFooter } from '../../../components/marketing/site-footer'; |
| 8 | import { SiteHeader } from '../../../components/marketing/site-header'; |
| 9 | import { TrackPageView } from '../../../components/marketing/track-page-view'; |
| 10 | import { getSessionUser } from '../../../lib/session'; |
| 11 | |
| 12 | interface 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. |
| 30 | const 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 | |
| 403 | export 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 | |
| 418 | export function generateStaticParams(): { source: string }[] { |
| 419 | return Object.keys(SOURCES).map((source) => ({ source })); |
| 420 | } |
| 421 | |
| 422 | export 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'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'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 | |
| 617 | function effortLabel(effort: 'free' | 'auto' | 'manual'): string { |
| 618 | return effort; |
| 619 | } |
| 620 | |
| 621 | function 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 | } |