# The runtime

> The anatomy of the snoozestack runtime: one service holding your project's functions, database, storage, queues, schedules, signals, and sites.

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

Everything your project's backend does — running functions, holding the database, storing files, working queues, firing schedules, recording signals, serving sites, verifying who's calling — is **one self-contained service**: the snoozestack runtime. `snoozestack dev` runs it on your machine as a single native process, and `snoozestack publish` runs the same thing on managed hosts. There is no separate database server, auth container, or job worker to assemble; the runtime is the whole anatomy.

**Diagram:** snoozestack runtime — one process · Agents · drive every layer · CLI · MCP · management API · App · what users meet · sites · domains · SDKs · signals · Auth · who may do what · sign-in · session verification · permissions · secrets · Functions · (req, capabilities) · HTTP · queues · schedules · webhooks · MCP tools · Data · state the runtime owns · database · storage · Portal · hosted, central · email relay · console · publish · placement · sign-in and sessions live · in the runtime's auth.db · the same process, three places · snoozestack dev · your machine · Hosted container · one per project · Hosted · snoozestack publish

**Why one service**

A conventional backend is a committee: a database here, an auth service there, a job runner, an object store, and a pile of configuration wiring them together — which is why “works on my machine” and “works in production” are usually two different claims. Making the whole backend one process makes the two claims the same one: what your laptop runs and what the hosted fleet runs are the same code path, differing only in configuration.

It also makes the project portable. The runtime reads your committed `snoozestack/` folder and keeps all of its state under one directory — so moving, replicating, or throwing away a backend is moving, replicating, or throwing away a folder, not an infrastructure migration.

### The layers, bottom up

**Data.** The base of the stack is what the runtime owns on disk: a database built from your committed [schema](https://snoozestack.com/docs/schema.md), one directory per declared [storage bucket](https://snoozestack.com/docs/storage.md), and the [signals](https://snoozestack.com/docs/signals.md) store. All of it lives under `.snoozestack/` — locally that's a folder you can delete to start fresh (`snoozestack dev reset`); hosted it's a volume that is continuously replicated to durable storage.

**Functions.** Above the state sits the only code that can touch it: [functions](https://snoozestack.com/docs/functions.md) you commit, on one contract — `export default (req, capabilities)`. Every entry point lands here: an HTTP call from your app, a [queue](https://snoozestack.com/docs/queues.md) message, a cron schedule, a webhook, an MCP tool call. There is no client-facing route to the database at all.

**Capabilities.** The seam between those two layers is explicit: a function declares in `project.toml` which capabilities it may use — `db`, `storage`, `auth`, `signals`, `push`, `secrets` — and receives exactly those as its second argument. An undeclared capability isn't forbidden at runtime; it simply isn't there. See [Permissions](https://snoozestack.com/docs/permissions.md).

**Identity.** Hosted projects serve magic-link, Google, Apple and password sign-in from their runtime, with users, sessions and signing keys in a separate, replicated `auth.db`. Verification checks the local signature and session row, so revocation is immediate. The portal relays outbound email. `snoozestack dev` runs the same plane from `.snoozestack/auth.db`, seeded with a test account, with a local account chooser standing in for Google and Apple and an outbox standing in for email. See [Authentication](https://snoozestack.com/docs/authentication.md).

**App.** The surface your users actually meet. Declared [sites](https://snoozestack.com/docs/websites.md) are served by the same process locally, next to the functions they call, and publish as immutable versions hosted; a [custom domain](https://snoozestack.com/docs/domains.md) fronts the same content; the [SDKs](https://snoozestack.com/docs/sdk-js.md) your app is built with call in; and what happens out there comes back as [Signals](https://snoozestack.com/docs/signals.md).

**Agents.** Because the project is files and the operations are explicit commands, the whole lifecycle is drivable by more than hands: the [CLI](https://snoozestack.com/docs/cli.md) for you and CI, the [management API](https://snoozestack.com/docs/management-api.md) for scripts, and [MCP](https://snoozestack.com/docs/mcp.md) for AI clients — all reading and writing the same project model, never a hidden setting.

### One process, three places

| Where | How it starts | What's different |
| --- | --- | --- |
| `snoozestack dev` | In-process, native — no Docker | Watches your files, serves the local dashboard, seeds fresh databases and the user@test.com/1234 account, and simulates the provider boundary (Google/Apple chooser, email and push outboxes) |
| Docker | Internal deployment detail | The hosted service runs each project in an isolated runtime container |
| Hosted | `snoozestack publish` | One container per project and namespace; state replicated continuously; watcher, dashboard, seed and the seeded account off; real providers, SES and APNs at the boundary |

The differences are configuration, not forks — hosted mode is the same engine with the development conveniences turned off and durability turned on. That's the property the rest of the docs lean on: if it worked under `snoozestack dev`, publishing doesn't change what runs, only where.

### What runs locally, service by service

Every service the runtime offers is one of four things under `snoozestack dev`: the *same code* as hosted; the same code down to a named boundary and a *local simulation* below it (a local account chooser instead of Google, an outbox instead of SES or APNs, a SQLite queue with SQS's shape instead of SQS); or a surface that exists in one mode only, with the reason. This table is generated from the runtime's own parity registry, whose test fails when a route or capability is left undeclared — so it is the code's claim, not the docs'.

| Surface | Locally | Notes |
| --- | --- | --- |
| `/` | Local only | The local dashboard at http://localhost:8787; hosted the portal is the console. Hosted, the console is the portal; the runtime's / is a text index and the browser page is never served on a project host. |
| `/_dashboard/*` | Simulated below a boundary | Same JSON routes and the same identity plane behind them; hosted they are gated by the portal's per-app token, locally by loopback and a per-run write token. The Emails and Notifications outbox views exist locally only. |
| `/_storage/upload/*` | Simulated below a boundary | capabilities.storage.signedUploadUrl() returns a URL the browser PUTs to in both modes; hosted it is a presigned S3 URL, locally the dev server verifies the signature and writes the file itself. |
| `/_storage/*` | Simulated below a boundary | Same route; hosted it redirects to a portal-signed S3 URL, locally it serves the file from .snoozestack/storage with a locally signed URL that survives a restart. |
| `/storage/v1/object/public/*` | Same code | Accepted in both modes for URLs stored before the runtime served storage; resolves to /_storage. |
| `/signals/v1/events` | Same code | signals-http.js in both modes. The anon key is checked upstream by the project host hosted and by the dev server locally; the ingestion quota is hosted-only (see below). |
| `/signals/v1/query` | Same code | signals-http.js in both modes, same key rule as ingest. |
| `/_queues/*` | Simulated below a boundary | Hosted a queue is SQS, drained on a 10-second tick with a visibility timeout, receive counts and a per-queue dead-letter queue. Locally dev-queue.js reproduces that shape in .snoozestack/queues.db (async by default; [dev] queues = "immediate" for tests). |
| `/_webhooks/*` | Simulated below a boundary | Hosted an API Gateway URL lands the body in SQS; locally POST /_webhooks/<name> enqueues into the same local queue and answers 202 the same way. |
| `/_sites/*` | Local only | Locally each declared site also gets its own port serving it at the root, with /api proxied to the runtime. Hosted, sites upload to S3 and are served by CloudFront on the project host; the runtime never serves site files there. |
| `/api/*` | Local only | Only the origin changes between a hosted URL and a local one. Hosted the project host strips /api before a request reaches the runtime; locally the dev server strips it itself so a URL written for https://<ref>.snzzz.com/api/… works with only the origin swapped. |
| `/mcp/v1/*` | Local only | An MCP client configured with the production URL shape can be pointed at localhost. Hosted the portal maps /mcp/v1/<name> onto the function; locally the dev server does, for mcp = true functions only. |
| `/functions/v1/*` | Same code | The SDK's URL shape, answered by the runtime in both modes. |
| `/<function>` | Same code | What the portal dispatches hosted; the same route locally. |
| `/auth/*` | Simulated below a boundary | hosted-identity.js in both modes — users, passwords, magic links, OAuth state, sessions, rate limits, self-delete — on auth.db. Locally the provider is a local account chooser (Google/Apple/GitHub) and a dev-minted id_token, the seeded user@test.com/1234 account is present (a preview has it too), and the anon key is the local one. |
| `/push/*` | Same code | hosted-push.js on auth.db in both modes; register and unregister work locally with the local anon key. |
| `/_dev/*` | Local only | GET /_dev/auth, POST /_dev/auth/reset, /_dev/oauth/<provider>, POST /_dev/schedules/<name>/run — loopback only. The seeded account's state and reset, the local provider page and the local schedule trigger are test and developer conveniences; a hosted container never registers the prefix. |
| `db` | Same code | One SQLite file, one single-statement contract; hosted the file is replicated by litestream underneath. |
| `secrets` | Simulated below a boundary | Hosted the values arrive as a file mounted at container start; locally they are read from .snoozestack/secrets.json (snoozestack secrets set --dev), with the linked project's dev namespace as a fallback that is cached into the file. |
| `storage` | Simulated below a boundary | Same five operations plus signed read and upload URLs; hosted against the project's S3 bucket through portal-signed URLs, locally against a directory under .snoozestack. |
| `auth` | Same code | The identity plane verifies the bearer session in both modes; locally a session the plane does not know falls through to the linked hosted project's JWKS. |
| `push` | Simulated below a boundary | Hosted the portal's sender signs to APNs with the project's key; locally the send resolves the same registry and is recorded to .snoozestack/outbox/push for the Notifications page (--push=sandbox keeps the hosted sandbox gateway). |
| `signals` | Same code | One store (signals.db) written the same way in both modes. |
| `email` | Simulated below a boundary | Hosted the plane asks the portal's relay to send the rendered template through SES; locally the same call renders the project's templates and writes the message to .snoozestack/outbox/email for the Emails page. |
| `function-loader` | Same code | One esbuild bundle definition: publish writes it into the published tree, and the dev server builds the same bundle in memory on the first request and again when the import graph changes. |
| `schedules` | Same code | The same cron matcher and timezone handling; hosted the control worker is the clock, locally the dev server ticks every 15 seconds, and `schedules run <name> --dev` fires one now. |
| `signals-quota` | Hosted only | Hosted only. The per-day quota and retention window are the account's plan limits, relayed to the container by the node agent; they are not knowable offline, and a local store is never purged. |
| `replication` | Hosted only | Hosted only; `snoozestack dev reset` is the local lifecycle. litestream replicates data.db, auth.db and signals.db from the container to S3; a laptop's .snoozestack is the developer's own and is never replicated anywhere. |

### Hosted runtime containers

The hosted service runs each project in an isolated runtime container. Docker deployment is currently an internal hosting detail; a public runtime image and standalone self-hosting configuration are not available.

### The isolation model

Hosted, each project×namespace gets its own container, and the container boundary is the tenant boundary: a runtime is scoped to exactly one app, capabilities are the only path from function code to data and secrets, and the blast radius of anything a function does is its own container. Namespaces — `dev` locally, `default` (live) and named previews hosted — each hold genuinely separate state, which is what lets you exercise a preview without touching live data.
