# Functions

> Committed handlers on the (req, capabilities) contract — the boundary of your application.

[HTML version](https://snoozestack.com/docs/functions/) · [Docs index](https://snoozestack.com/llms.txt)

Functions are your project's server-side code: TypeScript (or JavaScript) handlers on one contract — `export default (req, capabilities)` — declared in `project.toml` and run by the [runtime](https://snoozestack.com/docs/runtime.md). Each lives in its own directory under `snoozestack/functions/`, with shared code in `_shared/`. `req` is a web-standard `Request`; the return value is a web-standard `Response`; `capabilities` is everything the function declared it may reach.

**Why use it**

Every app needs backend code here, in a way that isn't true elsewhere: a client never talks to your tables directly — there is no such route — so a function is what reads or writes them, full stop. Beyond that baseline, the classic tests still apply: *does this need a secret?* (anything shipped to a browser or app binary is public) and *does this enforce a rule the caller can't be allowed to skip?* (a rule the client enforces isn't enforced at all).

The capability declaration is what makes a function reviewable at a glance. The diff that adds `send-digest` shows it can read the database and send push — and therefore that it cannot touch storage or read the payment key, because undeclared capabilities don't exist inside the handler. See [Permissions](https://snoozestack.com/docs/permissions.md).

And because the runtime runs whole on your machine, a function isn't “cloud code” you test by deploying: it's code you run, with its queues, schedules, and failure paths, before anything is published.

### Writing one

```bash title="snoozestack/project.toml"
[[functions]]
name = "chat"
entry = "chat/index.ts"
auth = "required"
capabilities = ["db", "auth", "secrets"]

[[secrets]]
name = "OPENAI_API_KEY"
functions = ["chat"]        # only this function may read it
```

```ts title="snoozestack/functions/chat/index.ts"
export default async function handler(req: Request, capabilities) {
  const user = capabilities.auth.user;             // auth = "required" guarantees one
  const { messages } = await req.json();

  const key = capabilities.secrets.OPENAI_API_KEY; // declared above, injected here
  // … call your model, stream a response, etc.

  capabilities.db.query(
    "insert into chats (user_id, at) values (?, ?)",
    [user.id, new Date().toISOString()],
  );
  return Response.json({ ok: true });
}
```

```bash title="terminal"
snoozestack dev        # POST http://localhost:8787/chat — live-reloads on edit
snoozestack publish    # POST https://<ref>.snzzz.com/api/functions/v1/chat
```

Editing the file takes effect on the very next local request. Streaming and SSE responses work; console output and errors land in [logs](https://snoozestack.com/docs/logs.md), and anything you `capabilities.signals.emit()` lands in [Signals](https://snoozestack.com/docs/signals.md).

### Dependencies

A function may import any npm package installed in your project. Install it at the project root and import it by name; nothing else is declared.

```bash title="terminal"
npm install zod
```

```ts title="snoozestack/functions/chat/index.ts"
import { z } from "zod";
import { requireUser } from "../_shared/auth.ts";
```

There is one build step, and it runs everywhere. `snoozestack publish` bundles each function with esbuild — its relative imports and its npm dependencies from your `node_modules` — into one file that ships alongside your source, and the hosted runtime runs that bundle. `snoozestack dev` runs the very same build in memory: a function is bundled on its first request and again whenever any file in its import graph changes, so an edit under `functions/_shared/` or a package update is served on the next request, no restart. `snoozestack check` runs it too. Because the step is shared, what dev accepts and refuses is exactly what hosted accepts and refuses: esbuild's TypeScript (parameter properties, enums and the rest — types are stripped, never checked), only `node:*` imports left external, packages that need a native addon refused, and a hard cap of 4MB per bundled function — a package that is not installed or a bundle over the cap fails on the first local request, by function name, with the message publish would print. Stack traces name your source file and line in both places.

- **Calling a third-party API that needs a secret** — An LLM, a payment provider, an email service. The key is a declared `[[secrets]]` entry readable only by the functions you name — the client calls your function, your function calls the vendor.
- **Business rules that must be trusted** — Applying a discount code, awarding credits, moving money. The client asks; the function decides, validates, and writes through `capabilities.db`.
- **Reacting to events** — A [queue](https://snoozestack.com/docs/queues.md) message, a cron schedule, or an inbound webhook triggers the same handler — nightly cleanups, digest emails, syncing an external system.
- **Giving AI clients tools** — An MCP endpoint (below) is a function that exposes *your product's* tools to Claude and other MCP clients, with your auth rules in front of it.

| Surface | Availability |
| --- | --- |
| Portal UI | Read-only code view per function with its auth mode and capabilities; Secrets and MCP sub-pages |
| CLI | `snoozestack check` validates; `snoozestack dev` runs; `snoozestack publish` ships; `secrets set/list/unset` manages values |
| SDK / HTTP | `snoozestack.functions.invoke()` / `POST /api/functions/v1/<name>` |

**Diagram:** Your app · functions.invoke() / HTTP · Schedule · [[schedules]] cron · Queue · [[queues]] functions = […] · Webhook · [[webhooks]] → queue · MCP client · mcp = true · The same function · (req, capabilities) · db · storage · push · declared capabilities · Secrets · declared [[secrets]]

### The capabilities

- `db` — `query(sql, params)` against the project database; see [Database](https://snoozestack.com/docs/database.md).
- `auth` — `user` (`{ id, email }` or `null`) and `anonymous`, from the verified session; see [Authentication](https://snoozestack.com/docs/authentication.md).
- `storage` — `put/get/list/delete/signedUrl/signedUploadUrl` on declared buckets; see [Storage](https://snoozestack.com/docs/storage.md).
- `signals` — `emit(type, name, props)`; see [Signals](https://snoozestack.com/docs/signals.md).
- `push` — `send(message)` through a declared notification service; see [Push Notifications](https://snoozestack.com/docs/push-notifications.md).
- `secrets` — the declared values this function is allowed to read, as plain properties.

### Secrets and schedules

Two things a function commonly needs are declared beside it rather than inside it. [Secrets](https://snoozestack.com/docs/secrets.md) name the credentials it may read — the value is set with the CLI and never committed — and [schedules](https://snoozestack.com/docs/schedules.md) put it on a cron timer, with the timing versioned alongside the code it triggers.

### MCP endpoints

A function with `mcp = true` on its `[[functions]]` entry speaks the **Model Context Protocol** over Streamable HTTP, so AI clients — Claude Code, Claude Desktop, anything MCP-capable — can call tools backed by your project. It is an ordinary function: same contract, same capabilities, same local run under `snoozestack dev`; the flag serves it at `/api/mcp/v1/<name>` — hosted on the project host, and locally at `http://localhost:8787/mcp/v1/<name>` (the dev server also answers the `/api/…` prefix, so an MCP config written for the hosted URL works against localhost with only the origin swapped) — and lists it on the console's **Functions → MCP** page.

This is how you expose *your product's* tools — the ones only your app knows how to provide. To let an AI client manage the *project* instead — edit schema, publish, set secrets — you don't write anything: every project already has a [management MCP endpoint](https://snoozestack.com/docs/mcp.md) with a token you create on the project Overview.

### HTTP API

```bash title="functions — /api/functions/v1"
curl -X POST "https://<ref>.snzzz.com/api/functions/v1/chat" \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" -d '{"messages": []}'
```

See [snoozestack-js](https://snoozestack.com/docs/sdk-js.md) for `snoozestack.functions.invoke()`, which attaches the signed-in session for you.
