page.tsx176 lines · main
1import Link from 'next/link';
2
3import { DocsShell } from '../../../components/shell';
4
5export const metadata = { title: 'migration · convex → briven' };
6
7export default function ConvexMigrationPage() {
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">convex → briven</h1>
16 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
17 port a convex.dev 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 only the convex-specific parts.
22 </p>
23
24 <Section title="schema port — the 90% rules">
25 <p>convex types map to briven schema dsl as follows:</p>
26 <Snippet>{`// convex/schema.ts
27defineTable({
28 email: v.string(),
29 status: v.union(v.literal('pending'), v.literal('active')),
30 createdAt: v.int64(),
31 ownerId: v.id('users'),
32 isPrimary: v.boolean(),
33});
34
35// briven/schema.ts
36import { bigint, boolean, schema, table, text } from '@briven/cli/schema';
37export default schema({
38 notes: table({
39 columns: {
40 email: text().notNull(),
41 status: text().notNull(), // union → text + app-level validation
42 createdAt: bigint().notNull(), // int64 / number → bigint (ms-since-epoch)
43 ownerId: text().references('users', 'id'), // v.id() → text + foreign key
44 isPrimary: boolean().notNull(),
45 },
46 }),
47});`}</Snippet>
48 <ul className="list-disc pl-5">
49 <li>
50 <code>v.union(v.literal(...))</code> — no enum helper today; use{' '}
51 <code>text()</code> and validate at the function layer.
52 </li>
53 <li>
54 <code>v.int64()</code> + <code>v.number()</code> for timestamps and money in cents
55 both → <code>bigint()</code>.
56 </li>
57 <li>
58 <code>v.id(&apos;users&apos;)</code> → <code>text().references(&apos;users&apos;, &apos;id&apos;)</code>.
59 </li>
60 <li>
61 <code>v.optional(X)</code> → drop the <code>.notNull()</code>.
62 </li>
63 <li>
64 convex&apos;s implicit <code>_creationTime</code> doesn&apos;t carry over — add an
65 explicit <code>createdAt: bigint().notNull()</code> if you need it.
66 </li>
67 <li>
68 indexes that convex declares with <code>.index(&quot;by_owner&quot;, [&quot;ownerId&quot;])</code>{' '}
69 move to the table&apos;s <code>indexes: [{`{ columns: ['ownerId'] }`}]</code> array.
70 </li>
71 </ul>
72 </Section>
73
74 <Section title="functions port">
75 <p>
76 convex&apos;s <code>query()</code> / <code>mutation()</code> / <code>action()</code>{' '}
77 map 1:1 onto the same names from <code>@briven/cli/server</code>:
78 </p>
79 <Snippet>{`// convex/notes.ts
80export const getNotes = query({
81 args: { authorId: v.id('users') },
82 handler: async (ctx, args) => {
83 return await ctx.db.query('notes').withIndex('by_owner', q => q.eq('ownerId', args.authorId)).collect();
84 },
85});
86
87// briven/functions/getNotes.ts
88import { query, type Ctx } from '@briven/cli/server';
89export default query(async (ctx: Ctx, args: { authorId: string }) => {
90 return await ctx.db('notes').select().where({ ownerId: args.authorId });
91});`}</Snippet>
92 <p>differences to know about up front:</p>
93 <ul className="list-disc pl-5">
94 <li>
95 <strong>file = function name.</strong> briven discovers functions by file basename;
96 export the handler as <code>default</code>. one function per file. convex packs many
97 into one file, so you&apos;ll split.
98 </li>
99 <li>
100 <strong>no validators in the wrapper.</strong> convex&apos;s <code>args:</code>{' '}
101 schemas don&apos;t exist; validate with zod inside the handler.
102 </li>
103 <li>
104 <strong>ctx.db is a typed query builder, not a sql escape hatch.</strong> see{' '}
105 <Link href="/functions" className="underline underline-offset-2">
106 /functions
107 </Link>{' '}
108 for the surface. for the rare query the builder doesn&apos;t cover, use{' '}
109 <code>ctx.db.execute(&apos;…&apos;, params)</code>.
110 </li>
111 <li>
112 <strong>no scheduler primitives yet.</strong> convex&apos;s{' '}
113 <code>ctx.scheduler.runAfter(...)</code> isn&apos;t in briven phase 1; use a{' '}
114 <code>pg_cron</code> entry or a brief sleep-and-poll loop in an action() handler
115 until the scheduler lands.
116 </li>
117 </ul>
118 </Section>
119
120 <Section title="data export from convex">
121 <p>
122 convex ships an export command that dumps every table to a single zip:
123 </p>
124 <Snippet>{`npx convex export --path ./convex-backup-$(date +%Y%m%d).zip`}</Snippet>
125 <p>
126 unzip and treat each per-table json file as a stream — the rows match the briven
127 column names you defined above. the import path is currently a small node script;{' '}
128 <code>briven import --from-convex &lt;zip&gt;</code> arrives with the public beta.
129 </p>
130 </Section>
131
132 <Section title="auth port">
133 <p>
134 convex auth (clerk / auth0 / custom) doesn&apos;t carry over — briven uses Better
135 Auth with magic-link + email/password + GitHub OAuth out of the box. plan for a
136 one-time forced sign-in on the cutover; users keep their email-as-identity but get
137 a fresh session.
138 </p>
139 <p>
140 if you need to preserve <code>userId</code> stability across the cut, set the
141 briven user&apos;s <code>id</code> to the convex user id during the data-import step
142 rather than letting briven mint a new ULID.
143 </p>
144 </Section>
145
146 <Section title="reactivity">
147 <p>
148 briven&apos;s <code>useQuery(&quot;getNotes&quot;, args)</code> on the client matches
149 convex&apos;s shape — same hook signature. under the hood briven runs LISTEN/NOTIFY
150 per touched table; convex uses its mutation log. tail latency is comparable; for
151 burst patterns where convex&apos;s log shines, briven realtime is a refactor target,
152 not a regression today.
153 </p>
154 </Section>
155 </DocsShell>
156 );
157}
158
159function Section({ title, children }: { title: string; children: React.ReactNode }) {
160 return (
161 <section className="mt-10">
162 <h2 className="font-mono text-lg">{title}</h2>
163 <div className="mt-2 space-y-3 font-mono text-sm text-[var(--color-text-muted)]">
164 {children}
165 </div>
166 </section>
167 );
168}
169
170function Snippet({ children }: { children: string }) {
171 return (
172 <pre className="overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs">
173 <code>{children}</code>
174 </pre>
175 );
176}