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, -).

Nothing to register. The files are the source of truth. Each publish reconciles the set: a new file is provisioned, a changed file updated, a deleted file torn down. Just run 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:

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).

Setting values: a 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.

Ask your AI: “add a 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().