schema dsl

declare your project's postgres schema in typescript. briven deploy diffs the file against the currently-deployed schema, generates a migration, and applies it transactionally on the data plane.

two paths

briven gives you two ways to build your database — they hit the same postgres schema and you can mix them freely:

  • cli + git — the canonical path. write briven/schema.ts below, commit it, briven deploy. migrations are diff-based and transactional. best for production work and code review.
  • dashboard / studio — open /dashboard/projects/<p>/studio, click + new table, pick column types, set primary keys / foreign keys / indexes. best for prototyping and one-off changes. studio has a copy as schema.ts button that emits the equivalent of this file from a live database, so anything you build by clicking can graduate to git later.

hello world

A schema lives at briven/schema.ts and exports a default value built with the schema() helper. each table is constructed with table(), and columns are declared with the typed builders.

import { bigint, schema, table, text } from '@briven/cli/schema';

export default schema({
  users: table({
    columns: {
      id: text().primaryKey(),
      email: text().notNull(),
      createdAt: bigint().notNull(),
    },
    indexes: [{ columns: ['email'], unique: true }],
  }),
});

column types

imported from @briven/cli/schema:

  • text() — variable-length utf-8. the default for strings.
  • varchar(n) — bounded text. n is required.
  • integer() — int4, the default for whole numbers.
  • bigint() — int8. use for timestamps-as-millis-since-epoch and money in cents.
  • boolean()
  • timestamp() — timestamptz. accepts a default like .default('now()').
  • uuid() — pg uuid. consider text() if you also use ulids.
  • jsonb() — typed via the column generic: jsonb<{ enabled: boolean }>().
  • vector(n) — pgvector embedding column. n is the dimension.

constraints

chained on a column builder:

  • .primaryKey() — at most one column per table; implies notNull + unique.
  • .notNull()
  • .unique() — single-column unique. for multi-column use indexes.
  • .default(value) — a literal or postgres expression. quoted strings need the inner quotes (.default("'EUR'") renders as DEFAULT 'EUR').
  • .references(table, column, opts?) — foreign key. opts.onDelete accepts 'cascade' | 'set null' | 'restrict' | 'no action'.

indexes

declared on the table, not on individual columns. multi-column indexes (compound or unique) live here:

uins: table({
  columns: {
    id: text().primaryKey(),
    uin: bigint().notNull(),
    ownerId: text().references('users', 'id'),
    status: text().notNull(),
  },
  indexes: [
    { columns: ['uin'], unique: true },
    { columns: ['status'] },
    { columns: ['ownerId', 'status'] },
  ],
}),

diff + apply

briven deploy walks the schema, compares it to the previous snapshot, and emits one of these change kinds: create_table, drop_table, add_column, drop_column. drops are refused unless you pass --confirm-destructive; the cli also prints a pre-migration snapshot tag so you can roll back with pg_restore from the audit trail if something goes wrong.

on apply, every table gets an implicit NOTIFYtrigger that fires on insert/update/delete — that's what powers the realtime useQuery story on the client side.

reserved names

briven prefixes its own platform tables with _briven_ on every project schema. you cannot define a table whose name starts with _briven_.