← migration

convex → briven

port a convex.dev project onto briven. follow the ten-step playbook on /migration — this page documents only the convex-specific parts.

schema port — the 90% rules

convex types map to briven schema dsl as follows:

// convex/schema.ts
defineTable({
  email: v.string(),
  status: v.union(v.literal('pending'), v.literal('active')),
  createdAt: v.int64(),
  ownerId: v.id('users'),
  isPrimary: v.boolean(),
});

// briven/schema.ts
import { bigint, boolean, schema, table, text } from '@briven/cli/schema';
export default schema({
  notes: table({
    columns: {
      email: text().notNull(),
      status: text().notNull(),                   // union → text + app-level validation
      createdAt: bigint().notNull(),               // int64 / number → bigint (ms-since-epoch)
      ownerId: text().references('users', 'id'),  // v.id() → text + foreign key
      isPrimary: boolean().notNull(),
    },
  }),
});
  • v.union(v.literal(...)) — no enum helper today; use text() and validate at the function layer.
  • v.int64() + v.number() for timestamps and money in cents both → bigint().
  • v.id('users')text().references('users', 'id').
  • v.optional(X) → drop the .notNull().
  • convex's implicit _creationTimedoesn't carry over — add an explicit createdAt: bigint().notNull() if you need it.
  • indexes that convex declares with .index("by_owner", ["ownerId"]) move to the table's indexes: [{ columns: ['ownerId'] }] array.

functions port

convex's query() / mutation() / action() map 1:1 onto the same names from @briven/cli/server:

// convex/notes.ts
export const getNotes = query({
  args: { authorId: v.id('users') },
  handler: async (ctx, args) => {
    return await ctx.db.query('notes').withIndex('by_owner', q => q.eq('ownerId', args.authorId)).collect();
  },
});

// briven/functions/getNotes.ts
import { query, type Ctx } from '@briven/cli/server';
export default query(async (ctx: Ctx, args: { authorId: string }) => {
  return await ctx.db('notes').select().where({ ownerId: args.authorId });
});

differences to know about up front:

  • file = function name. briven discovers functions by file basename; export the handler as default. one function per file. convex packs many into one file, so you'll split.
  • no validators in the wrapper. convex's args: schemas don't exist; validate with zod inside the handler.
  • ctx.db is a typed query builder, not a sql escape hatch. see /functions for the surface. for the rare query the builder doesn't cover, use ctx.db.execute('…', params).
  • no scheduler primitives yet. convex's ctx.scheduler.runAfter(...) isn't in briven phase 1; use a pg_cron entry or a brief sleep-and-poll loop in an action() handler until the scheduler lands.

data export from convex

convex ships an export command that dumps every table to a single zip:

npx convex export --path ./convex-backup-$(date +%Y%m%d).zip

unzip and treat each per-table json file as a stream — the rows match the briven column names you defined above. the import path is currently a small node script; briven import --from-convex <zip> arrives with the public beta.

auth port

convex auth (clerk / auth0 / custom) doesn't carry over — briven uses Better Auth with magic-link + email/password + GitHub OAuth out of the box. plan for a one-time forced sign-in on the cutover; users keep their email-as-identity but get a fresh session.

if you need to preserve userIdstability across the cut, set the briven user's id to the convex user id during the data-import step rather than letting briven mint a new ULID.

reactivity

briven's useQuery("getNotes", args)on the client matches convex's shape — same hook signature. under the hood briven runs LISTEN/NOTIFY per touched table; convex uses its mutation log. tail latency is comparable; for burst patterns where convex's log shines, briven realtime is a refactor target, not a regression today.