functions
your own typescript, next to your database, in a sandbox that can reach nothing else. off by default.
what this is
A function is a TypeScript module you deploy against one database. When it runs, it may talk to that database and it may compute. It cannot open a socket, read a file, see the host's environment, start a process, or import code from the internet. That is not a roadmap item. It is the security boundary.
Deploy from the dashboard at /functions. Invoke from the dashboard, or from a program over HTTP. There is no publish step and no separate runtime to provision.
Off by default, for every organisation, including ones created after this shipped. Running code we did not write is the most dangerous thing this product does, so the default is that you cannot. Ask us to switch on functions_runtime for your account. There is no setting you can flip yourself.
no outbound calls
The sandbox is spawned with network, filesystem, environment, subprocesses, native libraries and system calls all denied — and denied explicitly, so a later edit that adds an allow cannot quietly win. Remote imports and npm specifiers are also refused. The guest can compute, and it can ask its parent to run SQL. That is the whole of what it can do.
If you need to call Stripe, send email, or fetch a URL, this is not the place. Put that in your own application and keep the function for work that belongs next to the data.
A network blocklist is the wrong shape: anything not on the list is reachable. Here there is no list. The guest gets no network at all, so there is no DNS-rebinding trick to try and no hostname to add later.
what you write
Export a function. The runtime looks for a default export, or a named export matching the function's name. It is called with (ctx, args) — context first, then whatever the caller sent. The value you return must be JSON-serialisable.
typescript
export default async function (ctx, args) {
const rows = await ctx.db("notes")
.select(["id", "title"])
.where({ id: args.id })
.limit(1);
return rows[0] ?? null;
}ctx.db takes a table name, not a statement. Table and column names must look like PostgreSQL identifiers: a letter or underscore, then letters, digits or underscores, up to 63 characters. Names that would need quoting are refused rather than escaped.
- ctx.db(table).select(columns?)Read rows. Chain .where, .orderBy, .limit, .offset.
- ctx.db(table).insert(row | rows)Write one row or many. Chain .returning(columns).
- ctx.db(table).update(patch)Patch rows. Chain .where and .returning.
- ctx.db(table).delete()Delete rows. Chain .where and .returning.
- ctx.db(table).vectorSearch({…})Nearest neighbours on a vector column. distance is l2 (default), inner_product, or cosine. limit defaults to 10, hard cap 1,000.
- ctx.db.execute(sql, params?)A parameterised statement you wrote yourself. Placeholders are $1, $2 and so on.
- ctx.log.info / warn / error / debugLines you can read back on the invocation. Capped at 500, 2,048 characters each.
- ctx.authWho invoked, when the call came from a signed-in dashboard session. Null on the API-key door — a key is not a person.
- ctx.requestIdThis invocation's id.
Raw SQL, when you need it:
typescript
export default async function (ctx) {
const rows = await ctx.db.execute("select now() as at");
return { at: rows[0].at };
}Every ctx.db call in one invocation runs inside a single transaction. If the function throws, the writes roll back. Two invokes are two transactions — there is no BEGIN in one call and COMMIT in another.
how a program runs one
POST /api/v1/functions/<name>/invoke is the machine door. Bearer API key, same as sql over http. The organisation comes from the verified key and from nowhere else. There are no CORS headers: a key that works from a web page is a key published to every visitor of that page.
bash
curl -sS https://briven.tech/api/v1/functions/hello/invoke \
-H "Authorization: Bearer $BRIVEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"args":{"id":"n_1"},"database":"notes"}'response
{
"ok": true,
"value": { "id": "n_1", "title": "hello" },
"durationMs": 12,
"logs": [],
"touchedTables": ["notes"],
"logsDropped": 0
}database is optional when the key is pinned to one database, and required when it is not. A function that threw is still HTTP 200: the platform did its job, and the failure is yours, in the body, as ok: false with a code. A missing sandbox is the exception — that is our fault, and it is 503.
the limits
The count is of functions, not of invocations. Invocations are budgeted per minute on the route, because a monthly quota would be metering and metering is a different system.
- free plan3 functions, organisation-wide.
- starter plan20 functions.
- pro plan200 functions.
- enterpriseno cap on the number of functions.
- invocations60 per minute, per API key. Shared with the dashboard run button.
- wall clock30 seconds. After that the process is killed.
- heap128 MB of V8 old-generation heap. Long strings and typed arrays are not counted — this is not a cgroup.
- source256,000 characters. Past that it is not a function.
- rows per query10,000, matching SQL over HTTP.
- request body1,000,000 bytes on the invoke door.
when the door refuses
These are the HTTP refusals — the platform could not run the function. They are not the same as a function that ran and threw.
- 401 missing_api_keyNo Authorization header was sent at all.
- 401 invalid_api_keyMalformed, unknown, revoked or expired. One answer on purpose, so a key cannot be probed.
- 403 read_only_keyA read-only key may not run a function. A function is opaque — nothing can inspect the source and swear it only reads.
- 403 feature_disabledThe functions_runtime flag is off for this organisation. That is the default.
- 429 rate_limited60 invocations in 60 seconds on this key. Retry-After is in whole seconds.
- 404 function_not_foundNo function with that name is deployed to the database the key reached.
- 503 sandbox_unavailableThis host has no Deno binary, so nothing ran. That is our fault, which is why it is a 503 and not a 200.
when the function itself fails
HTTP 200, ok: false. The code is one of these.
- function_threwYour code threw. The message is yours.
- function_not_exportedThe module loaded but exported nothing callable — no default, and no named export matching the function's name.
- import_blockedThe module could not be imported — syntax error, or a forbidden import.
- network_blockedThe sandbox refused a network call.
- env_access_deniedThe sandbox refused an environment read.
- fs_access_deniedThe sandbox refused a file, process, or system call.
- invocation_timeoutThe process ran past 30 seconds and was killed.
- query_failedA ctx.db call failed against your own database.
what this is not
It is not Supabase Edge Functions, and it is not a general serverless host. Those can call the rest of the internet. This cannot. It is not the old @briven/cli deploy from a previous product either — that package is still on npm, it talks to a different system, and installing it will not deploy a function here. Deploy from the dashboard. Invoke over HTTP.