examples

copy-paste schemas for common app shapes. every example is a real briven/schema.ts — drop it in your project, run briven deploy, and you have a working data model. or click-build the equivalent in studio if you'd rather start from the dashboard.

todo app

one-user todo list. ships as `briven init --template todo-app`. minimal example to see reactive queries in action: every insert / update / delete from the cli or studio re-runs the active subscriptions on the client.

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

export default schema({
  todos: table({
    id: text().primaryKey(),
    body: text().notNull(),
    done: boolean().default('false').notNull(),
    createdAt: timestamp().default('now()').notNull(),
  }),
});

blog with comments

multi-user blog. posts belong to authors; comments belong to posts and can reply to other comments (self-FK on parent_id). createdAt is indexed on both tables for "newest first" feeds.

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

export default schema({
  authors: table({
    id: text().primaryKey(),
    email: text().notNull(),
    displayName: text().notNull(),
    createdAt: timestamp().default('now()').notNull(),
  }, {
    indexes: [{ columns: ['email'], unique: true }],
  }),

  posts: table({
    id: text().primaryKey(),
    authorId: text().notNull().references('authors', 'id'),
    title: text().notNull(),
    body: text().notNull(),
    published: boolean().default('false').notNull(),
    createdAt: timestamp().default('now()').notNull(),
  }, {
    indexes: [{ columns: ['authorId', 'createdAt'] }],
  }),

  comments: table({
    id: text().primaryKey(),
    postId: text().notNull().references('posts', 'id'),
    authorId: text().notNull().references('authors', 'id'),
    parentId: text().nullable().references('comments', 'id'),
    body: text().notNull(),
    createdAt: timestamp().default('now()').notNull(),
  }, {
    indexes: [{ columns: ['postId', 'createdAt'] }],
  }),
});

chat / dm app

real-time chat with rooms and members. members table is the join — a user belongs to a room with a role. messages reference both the room and the author so realtime fan-out scopes per-room.

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

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

  rooms: table({
    id: text().primaryKey(),
    name: text().notNull(),
    createdAt: timestamp().default('now()').notNull(),
  }),

  roomMembers: table({
    roomId: text().notNull().references('rooms', 'id'),
    userId: text().notNull().references('users', 'id'),
    role: text().default("'member'").notNull(),
    joinedAt: timestamp().default('now()').notNull(),
  }, {
    indexes: [
      { columns: ['roomId', 'userId'], unique: true },
      { columns: ['userId'] },
    ],
  }),

  messages: table({
    id: text().primaryKey(),
    roomId: text().notNull().references('rooms', 'id'),
    authorId: text().notNull().references('users', 'id'),
    body: text().notNull(),
    createdAt: timestamp().default('now()').notNull(),
  }, {
    indexes: [{ columns: ['roomId', 'createdAt'] }],
  }),
});

note: tip: the realtime subscriber filters by roomId, so server-side fan-out only pushes a message to the people in that room.

tiny e-commerce

products, carts, orders. money in integer cents (no floats!), stock tracked at the product level, orders cascade to order_items so deleting an order cleans up its line items.

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

export default schema({
  products: table({
    id: text().primaryKey(),
    name: text().notNull(),
    descriptionMd: text().notNull(),
    priceCents: integer().notNull(),
    stock: integer().default('0').notNull(),
    createdAt: timestamp().default('now()').notNull(),
  }),

  customers: table({
    id: text().primaryKey(),
    email: text().notNull(),
    displayName: text().notNull(),
  }, {
    indexes: [{ columns: ['email'], unique: true }],
  }),

  orders: table({
    id: text().primaryKey(),
    customerId: text().notNull().references('customers', 'id'),
    status: text().default("'pending'").notNull(),
    totalCents: bigint().notNull(),
    placedAt: timestamp().default('now()').notNull(),
  }, {
    indexes: [{ columns: ['customerId', 'placedAt'] }],
  }),

  orderItems: table({
    orderId: text().notNull().references('orders', 'id', { onDelete: 'cascade' }),
    productId: text().notNull().references('products', 'id'),
    quantity: integer().notNull(),
    unitPriceCents: integer().notNull(),
  }, {
    indexes: [{ columns: ['orderId', 'productId'], unique: true }],
  }),
});

note: always store money as integer cents. floats lose precision and break sums. the cli's integer() maps to int4 (up to ~$21M); use bigint() for totals that span many orders.

multi-tenant SaaS

org → projects → resources. every resource references the org it belongs to so a single WHERE clause enforces tenant isolation. consider also using row-level security if your runtime queries the database directly.

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

export default schema({
  orgs: table({
    id: text().primaryKey(),
    name: text().notNull(),
    createdAt: timestamp().default('now()').notNull(),
  }),

  users: table({
    id: text().primaryKey(),
    email: text().notNull(),
  }, {
    indexes: [{ columns: ['email'], unique: true }],
  }),

  orgMembers: table({
    orgId: text().notNull().references('orgs', 'id', { onDelete: 'cascade' }),
    userId: text().notNull().references('users', 'id'),
    role: text().default("'member'").notNull(),
  }, {
    indexes: [{ columns: ['orgId', 'userId'], unique: true }],
  }),

  resources: table({
    id: text().primaryKey(),
    orgId: text().notNull().references('orgs', 'id', { onDelete: 'cascade' }),
    name: text().notNull(),
    payload: text().notNull(),
    createdAt: timestamp().default('now()').notNull(),
  }, {
    indexes: [{ columns: ['orgId', 'createdAt'] }],
  }),
});

note: every query in your functions should start with `WHERE orgId = ?` where ? comes from the authenticated user's org membership. think of it like a partition key — make sure it's the leading column on every index.