page.tsx246 lines · main
1import { DocsShell } from '../../components/shell';
2
3export const metadata = { title: 'functions' };
4
5export default function FunctionsPage() {
6 return (
7 <DocsShell>
8 <h1 className="font-mono text-2xl tracking-tight">functions</h1>
9 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
10 every file under <code>briven/functions/</code> becomes an invocable endpoint at{' '}
11 <code>/v1/projects/:id/functions/:name</code>. functions run in a deno isolate per
12 project, with a typed db client and the project&apos;s env vars injected.
13 </p>
14
15 <Section title="hello world">
16 <p>
17 file name = function name. wrap your handler with <code>query</code>,{' '}
18 <code>mutation</code>, or <code>action</code> from <code>@briven/cli/server</code>:
19 </p>
20 <Snippet>{`// briven/functions/getNotes.ts
21import { query, type Ctx } from '@briven/cli/server';
22
23interface Args {
24 authorId: string;
25}
26
27export default query(async (ctx: Ctx, args: Args) => {
28 const rows = await ctx
29 .db('notes')
30 .select(['id', 'body', 'createdAt'])
31 .where({ authorId: args.authorId })
32 .orderBy('createdAt', 'desc')
33 .limit(50);
34 return { notes: rows };
35});`}</Snippet>
36 <p>
37 invoke locally after deploy with <code>briven invoke getNotes --body &apos;{`{"authorId":"u_..."}`}&apos;</code>.
38 </p>
39 </Section>
40
41 <Section title="client sdks">
42 <p>
43 framework hooks/stores/composables for invoking briven functions and subscribing to
44 their reactive results:
45 </p>
46 <ul className="list-disc pl-5">
47 <li>
48 <code>@briven/client</code> — framework-agnostic browser client (vanilla JS).{' '}
49 <code>createBrivenClient({`{...}`})</code> returns an object with{' '}
50 <code>.invoke()</code> and <code>.subscribe()</code>.
51 </li>
52 <li>
53 <code>@briven/react</code> — <code>BrivenProvider</code> +{' '}
54 <code>useQuery</code> / <code>useMutation</code> hooks.
55 </li>
56 <li>
57 <code>@briven/svelte</code> — <code>setBrivenClient</code> at boot;{' '}
58 <code>query</code> / <code>mutation</code> return Svelte stores.
59 </li>
60 <li>
61 <code>@briven/vue</code> — same surface, Vue 3 composables returning <code>Ref</code>s.
62 </li>
63 </ul>
64 </Section>
65
66 <Section title="query vs. mutation vs. action">
67 <ul className="list-disc pl-5">
68 <li>
69 <code>query()</code> — read-only. participates in the realtime subscription pipeline:
70 calling <code>useQuery(&quot;getNotes&quot;, args)</code> from the client will{' '}
71 re-run the function whenever a row changes in any table the function read.
72 </li>
73 <li>
74 <code>mutation()</code> — writes. wrapped in a transaction. the realtime fan-out fires
75 on commit.
76 </li>
77 <li>
78 <code>action()</code> — for outbound side effects (calling an external api, sending an
79 email). actions can&apos;t participate in subscriptions; their result isn&apos;t cached.
80 </li>
81 </ul>
82 </Section>
83
84 <Section title="the Ctx object">
85 <p>every handler receives a typed <code>ctx</code> as its first argument:</p>
86 <Snippet>{`interface Ctx {
87 db: DbClient; // typed query builder, project-scoped
88 requestId: string; // stable per request, correlated in logs
89 log: Logger; // structured json logger; never log customer data
90 env: Readonly<Record<string, string | undefined>>;
91 auth: { userId: string; tokenType: 'session' | 'api_key' } | null;
92}`}</Snippet>
93 </Section>
94
95 <Section title="db builder">
96 <p>
97 <code>ctx.db(table)</code> returns a chainable builder. the surface is a focused subset
98 — the 90% path of select / insert / update / delete:
99 </p>
100 <Snippet>{`// select
101await ctx.db('notes').select(['id', 'body']).where({ status: 'pinned' });
102
103// insert (returning is optional)
104const [row] = await ctx.db('notes').insert({ id, body }).returning();
105
106// update
107await ctx.db('notes').update({ status: 'archived' }).where({ id });
108
109// delete
110await ctx.db('notes').delete().where({ id });
111
112// raw escape hatch for anything the builder doesn't cover
113await ctx.db.execute('select count(*) from notes where created_at > $1', [cutoff]);`}</Snippet>
114 </Section>
115
116 <Section title="common patterns">
117 <p>
118 recipes for the things you&apos;ll write five times a week. all of them stay inside
119 the supported builder surface above — drop to <code>ctx.db.execute</code> only when
120 the case truly is one-off.
121 </p>
122
123 <h3 className="mt-4 font-mono text-sm text-[var(--color-text)]">cursor pagination</h3>
124 <p>
125 offset pagination breaks for fast-changing feeds (rows shift between pages). cursor
126 off the indexed sort column instead — typically <code>createdAt</code>:
127 </p>
128 <Snippet>{`interface Args { cursor?: string; limit?: number }
129
130export default query(async (ctx, args: Args = {}) => {
131 const limit = Math.min(args.limit ?? 50, 200);
132 let q = ctx.db('posts').select(['id', 'body', 'createdAt']);
133 if (args.cursor) q = q.where('createdAt', '<', new Date(args.cursor));
134 const rows = await q.orderBy('createdAt', 'desc').limit(limit + 1);
135 const next = rows.length > limit ? rows[limit - 1].createdAt.toISOString() : null;
136 return { rows: rows.slice(0, limit), nextCursor: next };
137});`}</Snippet>
138
139 <h3 className="mt-4 font-mono text-sm text-[var(--color-text)]">case-insensitive search</h3>
140 <p>
141 for prefix / fuzzy match across a small column use <code>ilike</code>; for full-text
142 search at scale, add a generated <code>tsvector</code> column and an index.
143 </p>
144 <Snippet>{`interface Args { q: string }
145
146export default query(async (ctx, args: Args) => {
147 const needle = '%' + args.q.replace(/[%_]/g, '') + '%';
148 return ctx.db('posts')
149 .select(['id', 'title'])
150 .where('title', 'ilike', needle)
151 .orderBy('createdAt', 'desc')
152 .limit(50);
153});`}</Snippet>
154
155 <h3 className="mt-4 font-mono text-sm text-[var(--color-text)]">transactional mutation</h3>
156 <p>
157 every mutation already runs inside a transaction — the runtime wraps your handler.
158 throws roll back the whole thing. if you need to fail explicitly, throw a{' '}
159 <code>brivenError</code> (see <em>errors</em> below).
160 </p>
161 <Snippet>{`export default mutation(async (ctx, args: { fromId: string; toId: string; amount: number }) => {
162 await ctx.db('accounts').decrement('balance', args.amount).where({ id: args.fromId });
163 await ctx.db('accounts').increment('balance', args.amount).where({ id: args.toId });
164 // both updates commit together. if the second throws, the first is rolled back.
165});`}</Snippet>
166
167 <h3 className="mt-4 font-mono text-sm text-[var(--color-text)]">user logs</h3>
168 <p>
169 <code>ctx.log(msg, fields?)</code> writes a structured entry to{' '}
170 <code>function_logs.user_logs_json</code>. it&apos;s the only thing surfaced in the
171 dashboard&apos;s logs tab, so use it instead of <code>console.log</code>:
172 </p>
173 <Snippet>{`export default mutation(async (ctx, args: { orderId: string }) => {
174 const order = await ctx.db('orders').select(['*']).where({ id: args.orderId }).first();
175 ctx.log('processing order', { orderId: order.id, total: order.totalCents });
176 // the dashboard logs tab will show this entry.
177});`}</Snippet>
178
179 <h3 className="mt-4 font-mono text-sm text-[var(--color-text)]">caller identity</h3>
180 <p>
181 <code>ctx.auth</code> is populated from the bearer key / session. use it as the
182 identity input to your authorization checks; never trust args for who the caller is.
183 </p>
184 <Snippet>{`export default query(async (ctx) => {
185 if (!ctx.auth) throw new brivenError('unauthorized', 'sign in to view', { status: 401 });
186 return ctx.db('notes').select(['id', 'body']).where({ ownerId: ctx.auth.userId });
187});`}</Snippet>
188 </Section>
189
190 <Section title="env vars">
191 <p>
192 set with <code>briven env put KEY value</code>. read inside a function as{' '}
193 <code>ctx.env.KEY</code>. encrypted at rest, decrypted only when the runtime spawns an
194 isolate, never logged.
195 </p>
196 </Section>
197
198 <Section title="errors">
199 <p>
200 throw to fail. the runtime serialises the error as <code>{`{ ok: false, code, message }`}</code>{' '}
201 and returns a 500. throw a structured error to set <code>code</code>:
202 </p>
203 <Snippet>{`import { brivenError } from '@briven/shared';
204
205export default mutation(async (ctx, args) => {
206 if (!args.body?.trim()) {
207 throw new brivenError('validation_failed', 'body is required', { status: 400 });
208 }
209 // ...
210});`}</Snippet>
211 </Section>
212
213 <Section title="lifecycle + scaling">
214 <p>
215 one deno isolate per project, warm-cached and pooled. cold start is under 200ms p50.
216 isolates are killed and replaced on crash, after 10 minutes idle, or after 1,000
217 invocations — whichever trips first. there is no cross-project state in any isolate.
218 </p>
219 <p>
220 outbound network is denied by default for rfc1918 (private), link-local, and the cloud
221 metadata endpoints. customer functions can call any public endpoint; bring up an
222 allowlist via the dashboard if you need stricter egress.
223 </p>
224 </Section>
225 </DocsShell>
226 );
227}
228
229function Section({ title, children }: { title: string; children: React.ReactNode }) {
230 return (
231 <section className="mt-10">
232 <h2 className="font-mono text-lg">{title}</h2>
233 <div className="mt-2 space-y-3 font-mono text-sm text-[var(--color-text-muted)]">
234 {children}
235 </div>
236 </section>
237 );
238}
239
240function Snippet({ children }: { children: string }) {
241 return (
242 <pre className="overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs">
243 <code>{children}</code>
244 </pre>
245 );
246}