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.