Functions
Ship a tiny server-side backend with your page: code that runs off-page, holds secret keys, and makes trusted writes the browser can't forge.
A static page plus quick.db covers a lot, but everything there runs in the browser, in plain sight. A function is the missing third leg: trusted JavaScript that runs on the server, so it can call a paid API with a secret key, tally a vote nobody can stuff, or validate input before it touches your data.
browser (public) → quick.fn("checkout") → your function (secret env, scoped db) → quick.db
One file is one function
Drop a file in a functions/ folder next to your page and it becomes a server-side endpoint. The filename is the name:
my-app/
index.html # your page (public, served from the edge)
functions/
checkout.js # → quick.fn("checkout")
nightly.js # → quick.fn("nightly")
lib/
stripe.js # NOT a function — a shared module the entrypoints import
Top-level files are entrypoints; anything in a sub-folder (e.g. functions/lib/) is shared code you import. Function source is stored server-side and never shipped to the browser — it's excluded from the public files. Names are route-safe (a–z, 0–9, -).
quickish (or republish from your dashboard).The handler
A function exports a default async handler. Return a Response, or just a plain object (sent as JSON) or a string:
// functions/hello.js
export default async function (req, { identity }) {
const me = identity(); // the signed-in visitor, or null
return { hello: me ? me.email : "world" };
};
The handler receives the request and a context object:
req—{ method, path, query, headers, body }.bodyis the parsed JSON the page sent (ornullfor GET).identity()— the signed-in visitor as{ email, tenant, site }, ornullif anonymous. Same identity the page sees.env— your secret values (see below).db— a scoped, trustedquick.db(see below).fetch— a guarded outbound fetch (off unless you allow it).
Return helpers mirror the web platform: Response.json(obj), Response.redirect(url), or new Response(body, { status, headers }).
Declare what it needs
A function says what it can touch with a statically-declared config — read at publish time, so it travels with the file (and with a remix):
// functions/checkout.js
export const config = {
env: ["STRIPE_SECRET"], // secret names it may read
db: { orders: "write", prices: "read" }, // collections + access
egress: ["api.stripe.com"], // hosts it may call out to
};
export default async function (req, { env, db, identity }) {
const me = identity();
const order = await db.collection("orders").create({ user: me?.email, items: req.body.items });
// env.STRIPE_SECRET is a real secret — the browser never sees it
return Response.json({ ok: true, id: order.id });
};
Secret env
List the names in config.env; set the values out of band so they're never in your code, your repo, or the browser. They're encrypted at rest and injected only at call time, and a function sees only the names it declared. Secret names are shell-style: A–Z, 0–9, _ (start with a letter).
quickish secrets command and a dashboard panel are landing shortly. Until then secrets are set through the CLI-authenticated API (POST /_cli/fn-secrets with { name, value }, owner/editor only). The point that matters: the value lives only on the server.The trusted writer (db)
The injected db is a quick.db pre-scoped to your config.db. It's the owner's trusted logic, so it can write owner-only collections a public visitor can't — but only the collections you named, and only read or write as declared ("write" implies read).
const open = await db.collection("orders").list({ where: { status: "open" }, limit: 20 });
const one = await db.collection("orders").get(id);
await db.collection("orders").create({ total: 42 });
await db.collection("orders").update(id, { status: "paid" }); // merges a patch
await db.collection("orders").remove(id);
This is what makes public-read / owner-write useful: the page renders data anyone can see, and the function is the only thing that can change it. See Permissions.
Calling out (egress)
Outbound fetch is off by default. List the hosts a function may reach in config.egress (["api.stripe.com", "*.openai.com"], or ["*"] for any). Calls are still capped, time-bounded, and blocked from private/internal addresses.
Call it from your page
From any page on the same site, quick.fn() invokes the function. It POSTs by default and resolves to parsed JSON (or text):
<script src="/quick.js"></script>
<script>
const res = await quick.fn("checkout", { items: cart });
if (res.ok) showReceipt(res.id);
// GET with no body:
const status = await quick.fn("status", null, { method: "GET" });
</script>
Limits
Each invocation runs in an isolated sandbox with comfortable caps — 64 MB memory, a 10 s wall-clock ceiling, up to 20 outbound calls, 1 MB request / 4 MB response — so a normal function never notices and a runaway one is contained. There's also a per-account rate ceiling (120/min, 2k/hour, 20k/day). A function error never breaks your page deploy; it surfaces as a warning.
functions/contact.js that emails me when someone submits the form, with my SendGrid key as a secret” — it writes the file, declares the config, and wires the form to quick.fn().