page.tsx250 lines · main
| 1 | import { DocsShell } from '../../components/shell'; |
| 2 | |
| 3 | export const metadata = { title: 'examples' }; |
| 4 | |
| 5 | interface Example { |
| 6 | id: string; |
| 7 | title: string; |
| 8 | description: string; |
| 9 | schema: string; |
| 10 | notes?: string; |
| 11 | } |
| 12 | |
| 13 | const EXAMPLES: readonly Example[] = [ |
| 14 | { |
| 15 | id: 'todo', |
| 16 | title: 'todo app', |
| 17 | description: |
| 18 | 'one-user todo list. optional starter via `briven setup --template todo-app` (or `briven init --template todo-app` for local files only). 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.', |
| 19 | schema: `import { boolean, schema, table, text, timestamp } from '@briven/cli/schema'; |
| 20 | |
| 21 | export default schema({ |
| 22 | todos: table({ |
| 23 | id: text().primaryKey(), |
| 24 | body: text().notNull(), |
| 25 | done: boolean().default('false').notNull(), |
| 26 | createdAt: timestamp().default('now()').notNull(), |
| 27 | }), |
| 28 | });`, |
| 29 | }, |
| 30 | { |
| 31 | id: 'blog', |
| 32 | title: 'blog with comments', |
| 33 | description: |
| 34 | '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.', |
| 35 | schema: `import { schema, table, text, timestamp, boolean } from '@briven/cli/schema'; |
| 36 | |
| 37 | export default schema({ |
| 38 | authors: table({ |
| 39 | id: text().primaryKey(), |
| 40 | email: text().notNull(), |
| 41 | displayName: text().notNull(), |
| 42 | createdAt: timestamp().default('now()').notNull(), |
| 43 | }, { |
| 44 | indexes: [{ columns: ['email'], unique: true }], |
| 45 | }), |
| 46 | |
| 47 | posts: table({ |
| 48 | id: text().primaryKey(), |
| 49 | authorId: text().notNull().references('authors', 'id'), |
| 50 | title: text().notNull(), |
| 51 | body: text().notNull(), |
| 52 | published: boolean().default('false').notNull(), |
| 53 | createdAt: timestamp().default('now()').notNull(), |
| 54 | }, { |
| 55 | indexes: [{ columns: ['authorId', 'createdAt'] }], |
| 56 | }), |
| 57 | |
| 58 | comments: table({ |
| 59 | id: text().primaryKey(), |
| 60 | postId: text().notNull().references('posts', 'id'), |
| 61 | authorId: text().notNull().references('authors', 'id'), |
| 62 | parentId: text().nullable().references('comments', 'id'), |
| 63 | body: text().notNull(), |
| 64 | createdAt: timestamp().default('now()').notNull(), |
| 65 | }, { |
| 66 | indexes: [{ columns: ['postId', 'createdAt'] }], |
| 67 | }), |
| 68 | });`, |
| 69 | }, |
| 70 | { |
| 71 | id: 'chat', |
| 72 | title: 'chat / dm app', |
| 73 | description: |
| 74 | '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.', |
| 75 | schema: `import { schema, table, text, timestamp } from '@briven/cli/schema'; |
| 76 | |
| 77 | export default schema({ |
| 78 | users: table({ |
| 79 | id: text().primaryKey(), |
| 80 | email: text().notNull(), |
| 81 | displayName: text().notNull(), |
| 82 | }, { |
| 83 | indexes: [{ columns: ['email'], unique: true }], |
| 84 | }), |
| 85 | |
| 86 | rooms: table({ |
| 87 | id: text().primaryKey(), |
| 88 | name: text().notNull(), |
| 89 | createdAt: timestamp().default('now()').notNull(), |
| 90 | }), |
| 91 | |
| 92 | roomMembers: table({ |
| 93 | roomId: text().notNull().references('rooms', 'id'), |
| 94 | userId: text().notNull().references('users', 'id'), |
| 95 | role: text().default("'member'").notNull(), |
| 96 | joinedAt: timestamp().default('now()').notNull(), |
| 97 | }, { |
| 98 | indexes: [ |
| 99 | { columns: ['roomId', 'userId'], unique: true }, |
| 100 | { columns: ['userId'] }, |
| 101 | ], |
| 102 | }), |
| 103 | |
| 104 | messages: table({ |
| 105 | id: text().primaryKey(), |
| 106 | roomId: text().notNull().references('rooms', 'id'), |
| 107 | authorId: text().notNull().references('users', 'id'), |
| 108 | body: text().notNull(), |
| 109 | createdAt: timestamp().default('now()').notNull(), |
| 110 | }, { |
| 111 | indexes: [{ columns: ['roomId', 'createdAt'] }], |
| 112 | }), |
| 113 | });`, |
| 114 | notes: |
| 115 | 'tip: the realtime subscriber filters by roomId, so server-side fan-out only pushes a message to the people in that room.', |
| 116 | }, |
| 117 | { |
| 118 | id: 'ecommerce', |
| 119 | title: 'tiny e-commerce', |
| 120 | description: |
| 121 | '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.', |
| 122 | schema: `import { bigint, integer, schema, table, text, timestamp } from '@briven/cli/schema'; |
| 123 | |
| 124 | export default schema({ |
| 125 | products: table({ |
| 126 | id: text().primaryKey(), |
| 127 | name: text().notNull(), |
| 128 | descriptionMd: text().notNull(), |
| 129 | priceCents: integer().notNull(), |
| 130 | stock: integer().default('0').notNull(), |
| 131 | createdAt: timestamp().default('now()').notNull(), |
| 132 | }), |
| 133 | |
| 134 | customers: table({ |
| 135 | id: text().primaryKey(), |
| 136 | email: text().notNull(), |
| 137 | displayName: text().notNull(), |
| 138 | }, { |
| 139 | indexes: [{ columns: ['email'], unique: true }], |
| 140 | }), |
| 141 | |
| 142 | orders: table({ |
| 143 | id: text().primaryKey(), |
| 144 | customerId: text().notNull().references('customers', 'id'), |
| 145 | status: text().default("'pending'").notNull(), |
| 146 | totalCents: bigint().notNull(), |
| 147 | placedAt: timestamp().default('now()').notNull(), |
| 148 | }, { |
| 149 | indexes: [{ columns: ['customerId', 'placedAt'] }], |
| 150 | }), |
| 151 | |
| 152 | orderItems: table({ |
| 153 | orderId: text().notNull().references('orders', 'id', { onDelete: 'cascade' }), |
| 154 | productId: text().notNull().references('products', 'id'), |
| 155 | quantity: integer().notNull(), |
| 156 | unitPriceCents: integer().notNull(), |
| 157 | }, { |
| 158 | indexes: [{ columns: ['orderId', 'productId'], unique: true }], |
| 159 | }), |
| 160 | });`, |
| 161 | notes: |
| 162 | '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.', |
| 163 | }, |
| 164 | { |
| 165 | id: 'multi-tenant', |
| 166 | title: 'multi-tenant SaaS', |
| 167 | description: |
| 168 | '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.', |
| 169 | schema: `import { schema, table, text, timestamp } from '@briven/cli/schema'; |
| 170 | |
| 171 | export default schema({ |
| 172 | orgs: table({ |
| 173 | id: text().primaryKey(), |
| 174 | name: text().notNull(), |
| 175 | createdAt: timestamp().default('now()').notNull(), |
| 176 | }), |
| 177 | |
| 178 | users: table({ |
| 179 | id: text().primaryKey(), |
| 180 | email: text().notNull(), |
| 181 | }, { |
| 182 | indexes: [{ columns: ['email'], unique: true }], |
| 183 | }), |
| 184 | |
| 185 | orgMembers: table({ |
| 186 | orgId: text().notNull().references('orgs', 'id', { onDelete: 'cascade' }), |
| 187 | userId: text().notNull().references('users', 'id'), |
| 188 | role: text().default("'member'").notNull(), |
| 189 | }, { |
| 190 | indexes: [{ columns: ['orgId', 'userId'], unique: true }], |
| 191 | }), |
| 192 | |
| 193 | resources: table({ |
| 194 | id: text().primaryKey(), |
| 195 | orgId: text().notNull().references('orgs', 'id', { onDelete: 'cascade' }), |
| 196 | name: text().notNull(), |
| 197 | payload: text().notNull(), |
| 198 | createdAt: timestamp().default('now()').notNull(), |
| 199 | }, { |
| 200 | indexes: [{ columns: ['orgId', 'createdAt'] }], |
| 201 | }), |
| 202 | });`, |
| 203 | notes: |
| 204 | '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.', |
| 205 | }, |
| 206 | ]; |
| 207 | |
| 208 | export default function ExamplesPage() { |
| 209 | return ( |
| 210 | <DocsShell> |
| 211 | <h1 className="font-mono text-2xl tracking-tight">examples</h1> |
| 212 | <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]"> |
| 213 | copy-paste schemas for common app shapes. every example is a real{' '} |
| 214 | <code>briven/schema.ts</code> — drop it in your project, run{' '} |
| 215 | <code>briven deploy</code>, and you have a working data model. or click-build the |
| 216 | equivalent in studio if you'd rather start from the dashboard. |
| 217 | </p> |
| 218 | |
| 219 | <nav className="mt-6 flex flex-wrap gap-2 font-mono text-xs"> |
| 220 | {EXAMPLES.map((e) => ( |
| 221 | <a |
| 222 | key={e.id} |
| 223 | href={`#${e.id}`} |
| 224 | className="rounded-md border border-[var(--color-border)] px-3 py-1 text-[var(--color-text-muted)] hover:text-[var(--color-text)]" |
| 225 | > |
| 226 | {e.title} |
| 227 | </a> |
| 228 | ))} |
| 229 | </nav> |
| 230 | |
| 231 | {EXAMPLES.map((e) => ( |
| 232 | <section key={e.id} id={e.id} className="mt-10 scroll-mt-20"> |
| 233 | <h2 className="font-mono text-lg tracking-tight">{e.title}</h2> |
| 234 | <p className="mt-1 font-mono text-sm text-[var(--color-text-muted)]"> |
| 235 | {e.description} |
| 236 | </p> |
| 237 | <pre className="mt-4 overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-code-bg)] p-4 font-mono text-xs text-[var(--color-code-text)]"> |
| 238 | <code>{e.schema}</code> |
| 239 | </pre> |
| 240 | {e.notes ? ( |
| 241 | <p className="mt-2 font-mono text-xs text-[var(--color-text-subtle)]"> |
| 242 | <span className="text-[var(--color-text)]">note: </span> |
| 243 | {e.notes} |
| 244 | </p> |
| 245 | ) : null} |
| 246 | </section> |
| 247 | ))} |
| 248 | </DocsShell> |
| 249 | ); |
| 250 | } |