# snoozestack documentation > Every page of https://snoozestack.com/docs in one file. The curated index — which page covers what — is at https://snoozestack.com/llms.txt, and each page is also served on its own at https://snoozestack.com/docs/.md. Pages are separated by a horizontal rule and open with their own H1. --- # Overview > snoozestack docs: quick start, the runtime, CLI, console, and client SDK reference [HTML version](https://snoozestack.com/docs/) · [Docs index](https://snoozestack.com/llms.txt) ## Overview **SnoozeStack** is a complete application platform you run locally, then publish to managed production servers. A **project** is a folder: a committed `snoozestack/` directory declaring its functions, schema, storage, queues, schedules, sites, and auth in one `project.toml`. One service — the [runtime](https://snoozestack.com/docs/runtime.md) — runs that whole declaration: natively on your machine under `snoozestack dev`, and on snoozestack's hosted fleet after `snoozestack publish`. Your organization's admin account owns the projects; each hosted project serves at `https://.snzzz.com`. You interact with the platform four ways: the [console](https://snoozestack.com/docs/portal-tour.md) (the hosted portal, and the identical local dashboard `snoozestack dev` serves), the [snoozestack CLI](https://snoozestack.com/docs/cli.md), the [snoozestack-js](https://snoozestack.com/docs/sdk-js.md) and [snoozestack-swift](https://snoozestack.com/docs/sdk-swift.md) SDKs inside your app — and your [AI client over MCP](https://snoozestack.com/docs/mcp.md), which can do everything the CLI can. **Diagram:** Your app · snoozestack-js / HTTP · .snzzz.com/api · this project's runtime · runtime container · Function · (req, capabilities) · capabilities.db · in-process, no network hop · capabilities.storage · objects + signed URLs · replicated · Durable storage · continuous backup · Auth routes on the same host (/auth/…) are answered by the runtime's identity layer; everything else lands on your functions. ## The mental model Two shifts orient everything else here. First, **the project is files**: what most platforms keep as dashboard settings — which functions exist, what they may access, the schema, the buckets, the queues, the cron timers — is a committed `project.toml` plus the code next to it, so configuration diffs, reviews, and rolls back like code, and nothing about your backend lives only in a console. Second, **your functions are the boundary**. Clients never reach the database — there is no client-facing table API to secure. A [committed function](https://snoozestack.com/docs/functions.md) reads and writes it through `capabilities.db`, and decides what the caller may see with an ordinary `where` clause informed by `capabilities.auth.user` — the check a hand-written API handler always made, minus the server to run. See [Permissions](https://snoozestack.com/docs/permissions.md). | You need to… | Use | Instead of | | --- | --- | --- | | Read and write your own data | A [function](https://snoozestack.com/docs/functions.md) with the `db` capability, called with `functions.invoke()` | A client-side query builder talking to a REST API | | Stop users seeing each other's rows | The function's own query, scoped to `capabilities.auth.user.id` | A row-level security policy applied automatically | | Change the schema | Edit `schema.mjs` and [publish](https://snoozestack.com/docs/schema.md) | Clicking in a console and hoping you remember | | Use a secret API key | A declared [secret](https://snoozestack.com/docs/functions.md), readable only by the functions you name | Shipping the key to the client | | Do slow work after responding | A [queue](https://snoozestack.com/docs/queues.md), declared in project.toml and testable locally | Blocking the request | | Store a file | [Storage](https://snoozestack.com/docs/storage.md), with the path in a column | A blob column in the database | | Ship it | `snoozestack preview` to try it in your own copy of the project, `snoozestack publish` to ship it as an immutable [release](https://snoozestack.com/docs/releases.md) | A deploy pipeline you assemble and babysit | | Let an AI client build and operate the backend | Its own [MCP endpoint](https://snoozestack.com/docs/mcp.md) | Pasting SQL out of a chat window | ## Organizations & projects Your admin login belongs to one **organization**, which owns every **project** you create — each a complete, isolated backend: one runtime with its own database, storage, queues, and users. See [Organizations & projects](https://snoozestack.com/docs/organizations-projects-sources.md) for the full model. **Diagram:** Organization · Project: my-app · one runtime · its own data · Project: admin-tool · one runtime · its own data · Project: mobile-app · one runtime · its own data ## Where to go next - New here? Start with [Quick start](https://snoozestack.com/docs/quickstart.md). - Want the architecture in one picture? Read [The runtime](https://snoozestack.com/docs/runtime.md). - Working with an AI client? Connect it to the project's [MCP endpoint](https://snoozestack.com/docs/mcp.md) — it gets these docs and every management operation over one connection. - Building an app against it? Start with the [JavaScript](https://snoozestack.com/docs/sdk-js.md) or [Swift](https://snoozestack.com/docs/sdk-swift.md) SDK. - Reading these as an agent? Every page here is also served as markdown — this one at [/docs.md](https://snoozestack.com/docs.md), the rest at `/docs/.md` — indexed at [/llms.txt](https://snoozestack.com/llms.txt), and concatenated at [/llms-full.txt](https://snoozestack.com/llms-full.txt). --- # Quick start > Install the CLI, run the whole backend locally, publish it, and connect an AI client. [HTML version](https://snoozestack.com/docs/quickstart/) · [Docs index](https://snoozestack.com/llms.txt) From zero to a running application backend in three commands — and to a hosted one in five. Everything the backend is lives in your project folder, so the loop is: scaffold the files, run them locally, publish the same files. ### 1. Install the CLI ```bash title="terminal" # version-pinned tarball, served from snoozestack.com (not the npm registry) npm install -g --ignore-scripts https://snoozestack.com/cli/snoozestack-1.56.0.tgz ``` ### 2. Scaffold and run locally `init` writes a `snoozestack/` folder — `project.toml` (the complete, portable definition of the application), a starter function, and a `schema.mjs`. `dev` runs the whole [runtime](https://snoozestack.com/docs/runtime.md) natively — functions, database, storage, queues, schedules, signals, sites — with no Docker and no account needed yet: ```bash title="terminal" cd ~/code/my-app snoozestack init snoozestack dev # → dashboard: http://localhost:8787/ ``` The one link it prints is the local dashboard — the same console the hosted portal shows, over your local state. Edit a function and the next request runs the new code; edit `project.toml` or `schema.mjs` and it reloads in place. A committed `seed.mjs`, if you add one, gives every fresh checkout working data. Sign-in works locally too, on the same identity plane the hosted project runs, seeded with `user@test.com` / `1234` — see [Authentication](https://snoozestack.com/docs/authentication.md). ### 3. Create the hosted project and publish When you want it on the internet: sign in, create a project, link the folder to it, and publish. `preview` runs the files in your own temporary copy of the project, at its own URL; `publish` validates them, records them as an immutable [release](https://snoozestack.com/docs/releases.md), and deploys it to production. ```bash title="terminal" snoozestack user login snoozestack projects create my-app snoozestack link --project my-app # records it in snoozestack/project.toml snoozestack preview # optional: try it first # preview-@you: ready # https://preview--my-app.snzzz.com snoozestack publish # Published "my-app" — release r1 is production. → https://my-app.snzzz.com ``` ### 4. Connect your AI client Open the project's **Overview** in the console and create an **MCP access token**. The token is shown once, with a setup prompt you can paste into Codex, Claude, or another AI client. For Claude Code, that prompt gives you: ```bash title="terminal" claude mcp add --transport http snoozestack-my-app \ https://snoozestack.com/api/mcp/my-app \ --header "Authorization: Bearer sb_mcp_..." ``` Your assistant can now edit and validate the project files, publish, set secrets, and read signals — the same lifecycle you just ran by hand. These docs come down the same connection, so it follows the platform's patterns instead of guessing at them. [MCP](https://snoozestack.com/docs/mcp.md) has the full tool catalogue and what a token can't reach. ### 5. Talk to it from your app Paste this into the AI client you connected in step 4. It has the project's management tools and documentation, so it can adapt the integration to the app that's actually in your folder. ```bash title="Prompt for your AI client" Create a todos web app with authentication. ``` --- # Installation > Install the snoozestack CLI, JavaScript SDK, and Swift SDK. [HTML version](https://snoozestack.com/docs/installation/) · [Docs index](https://snoozestack.com/llms.txt) The CLI and the JavaScript SDK are served from `snoozestack.com` as version-pinned npm tarballs — nothing is on the npm registry. The Swift SDK is a public GitHub repo you add as a Swift Package Manager URL. Your organization's admin account is your login everywhere: the console, the CLI, and the management API. ### CLI ```bash title="terminal" npm install -g --ignore-scripts https://snoozestack.com/cli/snoozestack-1.56.0.tgz snoozestack --help ``` ### JavaScript SDK — snoozestack-js ```bash title="terminal" npm install https://snoozestack.com/js/snoozestack-js-1.14.0.tgz ``` A typed, dependency-free client for your project's identity, functions, queues, and Signals — plus the `defineTable()` schema vocabulary the runtime builds your database from. Runs anywhere modern JavaScript does: Node 18+, browsers, Vite/Next.js apps. See [snoozestack-js](https://snoozestack.com/docs/sdk-js.md). ### Swift SDK — snoozestack-swift In Xcode: **File → Add Package Dependencies…**, paste the URL below, and add the `SnoozeStack` library to your app target. Or in a `Package.swift`: ```swift title="Package.swift" dependencies: [ .package( url: "https://github.com/raliworks/snoozestack-swift.git", from: "1.13.0" ), ], targets: [ .target(name: "MyApp", dependencies: [ .product(name: "SnoozeStack", package: "snoozestack-swift"), ]), ] ``` A Swift client for iOS, macOS, tvOS, and watchOS. See [snoozestack-swift](https://snoozestack.com/docs/sdk-swift.md). --- # 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/ 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://.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/ 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. | | `/` | 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/, POST /_dev/schedules//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 --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. --- # Releases & previews > snoozestack preview runs your checkout in your own temporary copy of the project; snoozestack publish ships it to production as an immutable release; publish --release rolls back. [HTML version](https://snoozestack.com/docs/releases/) · [Docs index](https://snoozestack.com/llms.txt) Three commands. Make changes locally, run `snoozestack preview` to try them in a temporary copy of the project, run `snoozestack publish` to ship them. Nothing sits in between, and nothing assumes git. | Command | What it does | | --- | --- | | `snoozestack preview` | runs your checkout in your own temporary copy of the project, at its own URL | | `snoozestack publish` | cuts an immutable release from your checkout and deploys it to production | | `snoozestack releases` | lists what publish has recorded; `publish --release ` deploys one again | ### Previews A preview runs production's secret values with everything your checkout declares on top — its functions, schema, sites, storage, queues, schedules and `[auth]` (redirect URLs, providers, `confirm_signups`) — so what you test is what you will publish. `snoozestack logs runtime --preview` tails it: auth, functions and storage are one process, and that log is everything happening inside. It does not copy production's data: the database starts empty, or from what a `[preview.seed]` policy selects and anonymizes (below). It has its own users, its own storage, schedules that never auto-fire and queues with no dispatch trigger, and every email its runtime sends is captured in its outbox instead of delivered, so a sign-up flow can be walked end to end without a real inbox. ```bash title="terminal" $ snoozestack preview Creating preview-@ada of "acme"… … preview-@ada: building... ready https://preview-0f4a4c3e1c1e4bd29a3f1234567890ab-acme.snzzz.com expires in 30 minutes — running "snoozestack preview" again resets the clock sign in with user@test.com / 1234 (seeded account, owner of the 212 seeded accounts rows) seeded from production under policy v1: accounts 212, orders 1,930; emptied 3 schema plan: 2 changes against production's schema Production is unchanged. When it checks out: snoozestack publish ``` Each person has one preview per project, named after their account — `preview-@`, or a hash of the account email when there is no handle — so a project's preview list says who is working on what. Running `snoozestack preview` again updates yours in place: same URL, same database, same users. It expires 30 minutes after it last came up, and every run resets the clock; at expiry its container, data, queues, schedule rows and secret overrides are removed. ```bash title="terminal" snoozestack preview status # URL, health, seed outcome, schema plan, expiry snoozestack preview outbox # the sign-up link it "sent" snoozestack preview delete # clean it up now ``` ### Publishing `snoozestack publish` goes straight to production. It uploads every artifact dark, records them as one **release** — an immutable package naming one version of every artifact, with a checksum per file — and deploys it in the same step. No preview is required, nothing needs approving, and whoever can publish changes production. Nothing is live until the last step, so a publish that fails part-way leaves production as it was. ```bash title="terminal" $ snoozestack publish Publishing "acme" to production… … Published "acme" — release r12 is production. schema: pointer moved runtime: pointer moved to 3f9a1c2e site www: live runtime: healthy ``` A release records artifact *ids*, never a rebuild: the runtime tree (source plus bundles), `schema.json`, each declared site's version, a sha256 per runtime file, the schema plan against what production served when it was published, what it dropped, its declarations — queues, triggers, webhooks, notification services, schedules, auth config and templates, each applied as a create or update, never a delete — and who published it. The portal recomputes the checksums from what it stored before it records anything. There is no base check. Two people publishing from different checkouts overwrite each other: last write wins. ### Dropping columns and tables Removing a column or a table from `snoozestack/schema.mjs` and publishing drops it. That is your call, not the platform's. But the edit that destroys production data can be one commented-out line, and `publish` is the only place it is ever seen, so it says what it is about to drop — with how much each holds in production right now — and asks: ```bash title="terminal" $ snoozestack publish This release drops data: settings.outreach_paragraph (text, 1,204 non-null rows) leads.legacy_source (text, 0 non-null rows) Dropped data cannot be recovered by rolling back — an older release's code runs against the current schema. Continue? [y/N] ``` It never refuses: one keystroke proceeds, and `--yes` answers for a script. The release records what it dropped and `snoozestack releases` marks it. ### Rebuilding tables A foreign key, a unique key, `.notNull()`, a default or a type changing on an existing table rebuilds it, copying every row across (see [Schema](https://snoozestack.com/docs/schema.md)). `publish` shows each rebuild with the rows it keeps, and asks only when a type change converts values already stored: ```bash title="terminal" $ snoozestack publish This release rebuilds tables, copying every row across: lead_searches 1,204 rows preserved +campaign_id +campaign_id → campaigns.id leads 8,431 rows preserved +campaign_id -unique(owner_id, source, external_id) +unique(owner_id, campaign_id, source, external_id) +campaign_id → campaigns.id ``` When production's rows cannot take the new definition — NULLs under a new `.notNull()`, two rows sharing a new unique key, a reference to a parent that is gone — publish stops before uploading anything and says how many rows are in the way. Rolling back never rebuilds: an older release runs against the tables as they are. ### Releases and rollback ```bash title="terminal" snoozestack releases # r12 live 2026-09-18… ada@acme.test 13 runtime files… ⚠ drops settings.outreach_paragraph snoozestack releases show r12 # the full package: checksums, schema plan, drops, deployments snoozestack releases deployments # every publish and rollback: who, when, how production answered snoozestack publish --release r11 # deploy r11 again — a rollback ``` `publish --release` deploys that exact recorded package — its artifacts by id, never rebuilt from your checkout. Pointed at a release older than the one live it is recorded as a rollback. A rollback restores code, not data. Schema moves forward only: rolling back to a release that predates a drop gives you the old code running against a schema without the column, and nothing a later release added is dropped by rolling back past it. Restoring data is a separate, deliberate thing. ### Seeding a preview from production By default a preview starts with an empty database. Declare a `[preview.seed]` policy in `snoozestack/project.toml` and it starts from a copy of production's data instead — reduced and anonymized before it serves a single request: ```bash title="snoozestack/project.toml" [preview.seed] version = 1 roots = ["accounts"] # the rows that seed the preview… include = ["plans"] # …plus reference tables copied whole account = "accounts.owner_id" # optional: the test account owns these rows [preview.seed.where] accounts = "preview_seed = 1" # which root rows — a column you set on the accounts you tag [preview.seed.columns.accounts] email = "email" # every text column of a kept table is classified full_name = "person_name" notes = "free_text" plan = "none" # "none" is a classification, not an omission [preview.seed.columns.plans] name = "none" ``` | Kept | How | | --- | --- | | root rows | the rows each `where` predicate selects (all rows without one) | | their closure | every row they reference, and every row that references them, transitively | | `include` | every row of a reference table, without pulling in what points at it | | everything else | emptied | Classifications: `none`, `email`, `person_name`, `phone`, `address`, `free_text`, `url`, `ip`, `secret`. Each transform is one-way and shaped like what it replaces: an email is still an email, a unique column stays unique, and the same original maps to the same replacement within one seed so relationships across tables survive. Nothing recovers the original. The policy fails closed. `snoozestack preview` refuses to upload a byte while a kept table has an unclassified text column; a gap found at seed time (a column added since, a predicate that does not run) empties the copy rather than serving production rows. The outcome — which tables were kept, how many rows dropped, what was transformed, or why the seed refused — is on `preview status`. Every preview has a test account, `user@test.com` / `1234` — the one `snoozestack dev` seeds — and never production's users. Seeded rows are owned by production user ids that do not exist in the preview, so for an app that scopes rows to their owner, say which column holds that id and the test account takes it: `account = "accounts.owner_id"` in `[preview.seed]`. Signing in then lands on the seeded rows instead of an empty app. The account owns the rows of the id that owns the most of them — tag one owner's rows in the `where` predicate to make that exact. Only the id is reused: the account is minted fresh in the preview, with the test address and password. **Why use it** Preview, publish, and a list of what shipped. A preview named after you is a habit you can reach for in one command; a release is a numbered, checksummed package you can deploy again when something goes wrong; and the one thing that cannot be undone — dropping data — is shown to you, with counts, before it happens. - **Try it on real-shaped data** — Your preview runs your checkout against production's configuration and, with a seed policy, anonymized production rows — at its own URL, with its own users. - **Ship without ceremony** — One command from your working tree to production — no branch required, and nothing to wait on. - **Roll back in one line** — publish --release deploys an earlier package's exact bytes; the database keeps moving forward. --- # Organizations & projects > How organizations and projects relate on snoozestack. [HTML version](https://snoozestack.com/docs/organizations-projects-sources/) · [Docs index](https://snoozestack.com/llms.txt) ### Organization The account, billing, and admin boundary. Your admin login belongs to exactly one organization; that organization owns every project you create. Org-wide settings (its name, admin accounts) are edited from the console. ### Project A complete, isolated app backend: one [runtime](https://snoozestack.com/docs/runtime.md) with its own database, storage, queues, schedules, signals, sites, and users, served at `https://.snzzz.com` — a domain of its own, separate from snoozestack.com, so nothing a project serves shares an origin, a cookie scope or a site boundary with the console. What the project *is* lives in your repo — the committed `snoozestack/` folder — and the hosted side runs what you publish. An organization holds up to the number of projects its plan allows — one on Free, three on Starter, ten on Pro (see [pricing](https://snoozestack.com/pricing/)). One project per app is the common case, and nothing is shared between them — not data, not users, not sign-in providers. The allowance counts *running* projects: an owner can stop a project (its API and sites answer “paused”, nothing is deleted) to free a slot, and when an organization is over its allowance after a downgrade the newest projects are paused until there is room again. **Diagram:** Organization · Project: my-app · one runtime · its own data · Project: admin-tool · one runtime · its own data · Project: mobile-app · one runtime · its own data ### Namespaces Within a project, state is namespaced: `dev` is your machine, `default` is live, and each person's preview (`snoozestack preview`) gets its own. Each namespace holds genuinely separate data, storage, queues, and secret overrides — so exercising a preview never touches live rows, and a device registered in development can never receive a live push. ### Creating a project ```bash title="terminal" snoozestack projects create my-app snoozestack link --project my-app # records it in snoozestack/project.toml ``` See [Using the console](https://snoozestack.com/docs/portal-tour.md) for where each of these lives in the console. --- # Using the console > A section-by-section tour of the snoozestack console — hosted, and the identical local dashboard. [HTML version](https://snoozestack.com/docs/portal-tour/) · [Docs index](https://snoozestack.com/llms.txt) There is one console, in two places. Hosted, it's your organization's admin UI at `https://snoozestack.com` — the dashboard lists the organization's projects and provisions new ones. Locally, `snoozestack dev` serves the same pages over your local runtime at the link it prints. The two are built from the same components on purpose: what you learn in one is true in the other, and a page missing from either side is a bug, not a roadmap item. One principle explains what the console will and won't do: **where a committed file decides something, the console is read-only.** Functions, schema, buckets, queues, schedules, auth providers, and email templates are declared in your project — so the console shows them, links the file that owns them, and never offers an edit box that would drift from it. Inside a project, the left rail groups everything the same way these docs do: ### Project Overview The landing page for a project starts with the three setup steps: install and log in to the CLI, link your local project folder, then connect the management MCP. The project URL, API keys, and MCP access tokens all live directly below those steps. ### Data: Database, Storage **Database** has two pages, both read-only views over the runtime: **Schema** (every table, its columns and indexes — the shape your committed `schema.mjs` produced) and **Data** (browse a table's rows). Changing either goes through the files — see [Schema](https://snoozestack.com/docs/schema.md). **Storage** lists the project's declared buckets and their objects; files move with the CLI and [signed operations](https://snoozestack.com/docs/storage.md), not through an upload box. ### Platform: Authentication, Functions, Queues, Sites **Authentication** is **Users** — the project's own signed-up people, with delete and ban — and **Providers**, showing which sign-in methods the project declares (magic link, Google, Apple, email/password) and the one callback URL to register with each provider; see [Authentication](https://snoozestack.com/docs/authentication.md). **Functions** lists the declared functions with each one's code, auth mode, and capabilities read-only, plus sub-pages for **Secrets** (declared names and which namespaces override them — never values) and **MCP** endpoints. **Queues** shows each declared queue, the functions it dispatches to, its recent deliveries and its dead-letter queue, with **Notifications** for the push services a queue trigger can send through. **Sites** is **Websites** — published sites, their versions, and which one is live — and **Domains**, where a domain you own gets connected and its DNS records managed. See [Functions](https://snoozestack.com/docs/functions.md), [Queues](https://snoozestack.com/docs/queues.md), [Websites](https://snoozestack.com/docs/websites.md), and [Domains](https://snoozestack.com/docs/domains.md). ### Monitor: Signals **Signals** is everything the app reports, in one envelope — product events, logs, metrics, traces, audits — read as one log: facet the window by signal type and level, filter by message text, and watch the shape of activity with errors stacked on the histogram. The same store answers the CLI (`snoozestack signals`) and the query/export APIs — see [Signals](https://snoozestack.com/docs/signals.md) and [Logs](https://snoozestack.com/docs/logs.md). ### Project Settings Hosted only (the local dev server has no settings to edit): rename the project, and the danger zone where a project is deleted, permanently, along with everything in it. --- # Database > The database inside the runtime: schema from files, queries from functions. [HTML version](https://snoozestack.com/docs/database/) · [Docs index](https://snoozestack.com/llms.txt) Every project's database lives *inside* its [runtime](https://snoozestack.com/docs/runtime.md): a database whose shape comes from your committed [schema](https://snoozestack.com/docs/schema.md), whose only reader and writer is a [function](https://snoozestack.com/docs/functions.md) holding the `db` capability, and whose state — local, per preview, and live — is isolated per namespace. There is no database server to connect to and no client-facing query API to secure. **Why it lives in the runtime** The queries your functions run are in-process reads against local state — no connection pool, no network hop, no N+1 latency cliff. That is not a toy configuration: you get ordinary SQL and real transactional semantics without the operational surface of a database server, and hosted state is continuously replicated to durable storage rather than living on one disk. It also keeps the whole backend portable. Your data sits under `.snoozestack/` next to your storage and signals — one directory that runs identically on your machine, in a container, and hosted, and one directory to copy when you want your data somewhere else. It is a genuine exit, not an export in someone's bespoke shape. ### Reading and writing it A function granted the `db` capability gets one method — a parameterized, single-statement query: ```ts title="snoozestack/functions/notes/index.ts" export default async function handler(req: Request, capabilities) { const user = capabilities.auth.user; if (!user) return new Response("unauthorized", { status: 401 }); const { rows, columns } = capabilities.db.query( "select id, body, created_at from notes where user_id = ? order by created_at desc", [user.id], ); return Response.json({ columns, rows }); } ``` Writes return `{ changes, lastInsertRowid }` instead of rows. One statement per call is enforced — a second `;`-separated statement is an error, not a silent no-op — which is what makes string-building attacks against this surface boring. Always pass values as `params`, never interpolated into the SQL. ### Looking at the rows yourself While you're building, you usually just want to see what a function actually wrote. The console shows it at **Database → Data**, and the CLI prints the same rows: ```bash title="reading rows" snoozestack data tables # every table, with row and column counts snoozestack data rows notes # one page of rows (100), tab-separated snoozestack data rows notes --all # every page snoozestack data rows notes --json | jq '.rows[0]' snoozestack data rows notes --preview ada # that preview's own database ``` This reads over the [management API](https://snoozestack.com/docs/management-api.md), so it uses a management credential — your own login, or a revocable project token from `snoozestack projects tokens create` for CI. Deliberately *not* the project's anon or service key: those are app credentials, and the project's own API has no table endpoint for them to unlock. A client reaches data only through a [function](https://snoozestack.com/docs/functions.md) you committed, which is what makes a table holding third-party access tokens unreadable to anyone holding a key — see [Permissions](https://snoozestack.com/docs/permissions.md). Reads made with a project token are recorded in the project's [audit history](https://snoozestack.com/docs/portal-tour.md). | Surface | Availability | | --- | --- | | Portal UI | Read-only **Database → Schema** (tables, columns, indexes) and **Database → Data** (browse rows) — in both the hosted console and the local dashboard | | CLI | `snoozestack data tables` / `snoozestack data rows ` to read rows; `snoozestack schema build` for the schema, which is a committed file — see [Schema](https://snoozestack.com/docs/schema.md) | | SDK / HTTP | No direct database client, by design — apps call a [function](https://snoozestack.com/docs/functions.md) with `functions.invoke()` | ### Where the data lives - **Locally**: `.snoozestack/dev.db`, built from your schema on first run and kept in sync as you edit it. `snoozestack dev reset` throws it away and rebuilds (re-running your committed `seed.mjs`, if any). - **Hosted**: inside the project's runtime container, on a volume replicated continuously to durable storage — restore and host-failure recovery are platform responsibilities, not yours. - **Per namespace**: live (`default`) and each named preview hold separate databases, so a preview publish can be exercised against its own data. ### Your schema isn't locked in The `defineTable()` vocabulary is deliberately portable: every type has a defined mapping in more than one SQL engine, so the schema describes your data rather than an engine. If your product ever outgrows what the runtime provides, the schema — and the ordinary SQL your functions already write — travels with it. --- # Schema > defineTable() in schema.mjs is the schema — the only thing that changes a database's shape. [HTML version](https://snoozestack.com/docs/schema/) · [Docs index](https://snoozestack.com/llms.txt) A project's schema is a committed file: `snoozestack/schema.mjs`, plain JavaScript calling `defineTable()`. The [runtime](https://snoozestack.com/docs/runtime.md) keeps the database matching it — locally on every edit, hosted at publish. That file is the whole story: there are no migration files to write, order or replay. **Why use it** Your schema is part of your application, and it has to change in lockstep with the code that depends on it. The alternative — typing DDL into a SQL console whenever something needs adding — works exactly until the second environment or the second developer appears; then nobody can say what shape production is in. Here there is no SQL console to drift through: the file is the schema, it rides in the same commit as the code that needs it, and checking out an old revision gives you the schema that matched it. It also means there is no second source of truth to disagree with the first. A hand-written migration folder is a parallel history of the same information, and the two drift the moment someone edits one and not the other. Declaring the shape you want and letting the runtime work out the difference is the same bargain an ORM's schema file makes, without the SQL. ### Defining tables ```ts title="snoozestack/schema.mjs" import { defineTable, schema } from "snoozestack-js"; export const notes = defineTable("notes", { id: schema.uuid().primaryKey(), user_id: schema.uuid().notNull(), body: schema.text().notNull(), created_at: schema.timestamp().notNull(), }); ``` The type vocabulary — `text`, `int`, `bigint`, `real`, `boolean`, `timestamp`, `uuid`, `json`, `blob` — is deliberately portable: every type has a defined mapping in more than one SQL engine, so the schema describes your data, not an engine. Under `snoozestack dev`, saving this file updates the local database in place — new tables and columns appear without losing the data you already have — and rebuilds the committed `snoozestack/schema.json` beside it, printing what changed. Without `dev` running, `snoozestack schema build` does the same rebuild, and `snoozestack check` fails while `schema.json` disagrees with `schema.mjs`. | Surface | Availability | | --- | --- | | Portal UI | Read-only: **Database → Schema** shows the shape the runtime built; changes come from this file | | CLI | `snoozestack schema build` — the schema is a file; nothing pushes it on its own | | SDK / HTTP | `defineTable()`/`schema.*` are exported by snoozestack-js — the schema file is ordinary code | ### Changing the shape Edit `schema.mjs` and publish. The runtime works out the difference between what the database records as applied and what the file declares, and applies only that — in one transaction, so a failure leaves the database exactly as it was. Structural changes — create/drop table, add/drop column, create/drop index — apply directly. Everything inside an existing table's definition — a column's type, nullability or default, a foreign key, a unique or primary key — has no SQLite `ALTER`, so those rebuild the table: a new table with the new definition, every row copied across, the old one dropped, the new one renamed into place, its indexes recreated. No row is lost. What a rebuild can do is refuse: `.notNull()` over rows that are still NULL, a unique key over rows that already share one, a foreign key over rows whose parent is gone. `snoozestack publish` counts those *in production* before it uploads anything and names what is in the way, so the migration is effectively rehearsed against your real rows before it runs. Fix the rows, then publish again. A drop destroys data, so publish also shows what it would drop with how much each column or table holds, and asks first. ### Changing data, not just shape Some changes are not expressible as a shape: splitting one table into two, backfilling a new column from an old one, de-duplicating rows before a unique key can exist. Those take two releases — **expand**, then **contract**: 1. **Expand.** Add the new shape in `schema.mjs` — new table, or a new nullable column — and publish. Nothing is dropped, so old and new code both still work. 1. **Move the data.** Write a [function](https://snoozestack.com/docs/functions.md) that reads the old and writes the new, publish it, and call it once. If it will not finish inside a request, have it enqueue the work and let a [queue](https://snoozestack.com/docs/queues.md) drain it in batches. 1. **Contract.** Remove what is now unused from `schema.mjs` and publish again. The drop prompt tells you how many values each column still holds — which is also how you catch a backfill that did not actually run. It is two releases rather than one for a reason that applies whatever the mechanism: deploying is not instantaneous, so for a moment the old code is still serving. A single release that added the new column, moved the data and dropped the old one would break every request in flight. Expanding first means both shapes are valid at once, which is what makes the change safe to deploy at all. ### Applying it You never apply a schema by hand. Locally, `snoozestack dev` syncs the database on every save and prints what changed; hosted, `snoozestack publish` ships `schema.json` with the release and the project's runtime applies it as it starts, before it serves a request. There is no command that pushes DDL at a database, and no file anywhere that gets replayed. A rollback restores code, not data. `snoozestack publish --release ` redeploys an earlier release's exact artifacts but leaves the schema where it is — schema moves forward only, so a rollback never removes a column a later release added, and anything already dropped stays dropped. Recovering from a bad data change is a new release, not an undo. ### Seeding data Schema says what tables exist; a committed `snoozestack/seed.mjs` says what a fresh database starts with. It default-exports `async ({ db }) => {}` — the same `db` surface a function gets — and runs exactly once against a brand-new local database, so every clone of the repo starts with working data. `snoozestack dev reset` then `snoozestack dev` is the reseed path. ### An agent's path An MCP-driving agent working on your repo edits `schema.mjs` and runs the same commands — there is deliberately no tool that changes the schema behind the file's back. What the [MCP](https://snoozestack.com/docs/mcp.md) endpoint offers is the read side: `tables_list` returns the shape the runtime actually built, so an agent can check its edit landed after a publish. --- # Storage > Declared buckets, function-authorized access, and signed URLs. [HTML version](https://snoozestack.com/docs/storage/) · [Docs index](https://snoozestack.com/llms.txt) Object storage for user uploads — avatars, attachments, exports. Buckets are declared in `project.toml`, so what storage exists is a reviewable file; access goes through your [functions](https://snoozestack.com/docs/functions.md) via the `storage` capability, so *who may touch which path* is code you wrote. Locally each bucket is a real directory under `.snoozestack/storage/`; hosted, the same declarations are backed by managed object storage — your application code doesn't change. **Why use it** Files could technically go in the database as blob columns, and that is almost always the wrong call. A database is tuned for many small rows read transactionally; a 4 MB photo in a row bloats every backup and pushes useful data out of cache. Object storage is built for the opposite shape — few, large, immutable blobs, served directly. The pattern worth internalizing, because it's how essentially every production app handles uploads: **the bytes go to storage, and the database row holds only the path.** Your `messages` table stores `photo_path`, not the image. Queries stay small, the file is served without streaming through your handler, and the two are authorized independently. Access control is the same story as [Permissions](https://snoozestack.com/docs/permissions.md) everywhere else: there is no policy language to learn, because there is no client-facing write route to police. A function mints the signed operation, and the function knows who's asking. ### Declaring buckets ```bash title="snoozestack/project.toml" [[storage]] name = "avatars" public = true # objects readable at a stable URL [[storage]] name = "documents" # private (the default): reads need a signed URL ``` A bucket is its declaration: the runtime serves exactly the buckets the file declares, so `snoozestack publish` brings production's in line and `snoozestack preview` your preview's. There is no command that creates, flips or deletes a bucket — change the file and publish. Each namespace (dev, live, every preview) holds genuinely separate files. - **User-generated media** — Avatars, post images, message attachments. Have the function namespace the path by the caller's id (`/`) so every user gets their own area — enforced by the code that mints the path, in one place. - **Private documents** — Invoices, contracts — a private bucket plus short-lived signed URLs, so a leaked link stops working instead of exposing the file forever. - **Generated artifacts** — CSV exports, PDF reports, thumbnails: a function writes the file with `capabilities.storage.put()` and hands back a signed URL to download it. - **Public static assets** — Logos, product images — a public bucket serves them at a stable URL with no auth round-trip. (For a whole site of files, see [Websites](https://snoozestack.com/docs/websites.md) instead.) | Surface | Availability | | --- | --- | | Portal UI | Read-only bucket and object listing (hosted console and local dashboard alike); files move with the CLI and signed operations | | CLI | `snoozestack storage list/push/create/public/private/delete`, each scoped by `--dev`/`--preview` | | SDK / HTTP | No storage client, deliberately — the app asks your function, then PUTs/GETs the signed URL directly | **Diagram:** Client · wants to upload · ask · Your function · capabilities.storage · signed URL · Client PUTs bytes · direct, time-limited · Bucket · declared [[storage]] · Public bucket read · stable URL, no auth · GET /storage/v1/object/public/… · The function decides the path and whether this caller may write it; the bytes never stream through your handler. ### The capability surface A function declaring the `storage` capability can reach every declared bucket: ```bash title="inside a function" await capabilities.storage.put("avatars", `${user.id}/avatar.png`, bytes); const file = await capabilities.storage.get("avatars", key); const files = await capabilities.storage.list("avatars", `${user.id}/`); await capabilities.storage.delete("avatars", key); // Time-limited read for a private object: const url = await capabilities.storage.signedUrl("documents", key, { expiresIn: 3600 }); // Hosted: authorize an upload without the bytes touching your function: const upload = await capabilities.storage.signedUploadUrl("avatars", `${user.id}/avatar.png`); ``` Every method is `await`-able in both dev and hosted, so the same function code runs in both. `signedUploadUrl` is the path for large uploads — the client PUTs straight against it, so the bytes never pass through your function. Hosted that is a presigned S3 URL; under `snoozestack dev` it points at the dev server itself and works the same way (same PUT, same content-type rule, CORS answered for a site on its own port), with uploads capped at 64 MB locally. Signed URLs, read and upload, are minted with a key kept in `.snoozestack/`, so one issued before a dev restart is still valid after it; `snoozestack dev reset` rotates it. ### The usual pattern: upload, then store the path ```bash title="app code (snoozestack-js)" // 1. Ask your function for an upload slot. It namespaces the path by the // caller's own id — the client never chooses where it may write. const { data: slot } = await snoozestack.functions.invoke("photo-upload-url", { body: { contentType: file.type }, }); // 2. Bytes → storage, directly against the signed URL. await fetch(slot.url, { method: "PUT", body: file, headers: { "content-type": file.type } }); // 3. Path → database, via a function. The row stays tiny. await snoozestack.functions.invoke("post-message", { body: { body: text, photo_path: slot.path }, }); // 4. Reading back from a public bucket: build the URL from the stored path. const url = `${SNOOZESTACK_URL}/storage/v1/object/public/photos/${slot.path}`; ``` Store the *path*, not the full URL. Paths stay valid if the bucket's visibility changes, if you attach a custom domain, or if a private bucket later needs signed URLs — a full URL baked into a row has to be rewritten in all of those cases. ### Sharing files: public vs. signed URLs A **public** bucket serves objects at a stable URL, for anyone, forever — right for content that is genuinely public. A **private** bucket serves only through `signedUrl()`, whose links expire — right for anything where “who may see this” is a question. If you find a function proxying file bytes to apply a rule, mint a signed URL instead: the rule runs in the function, the bytes don't. --- # 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://.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/` | **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/` — hosted on the project host, and locally at `http://localhost:8787/mcp/v1/` (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://.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. --- # Queues > Declared queues, triggers that run functions for their messages, inbound webhooks, and dead letters. [HTML version](https://snoozestack.com/docs/queues/) · [Docs index](https://snoozestack.com/llms.txt) A queue is a durable to-do list your system writes to now and processes later. Declare one in `project.toml` — with the [functions](https://snoozestack.com/docs/functions.md) that should process its messages — and the same declaration runs in both places: `snoozestack dev` runs a local queue with the managed queue's shape (a send is accepted and delivered on the next tick, retried after a visibility window, and dead-lettered after `max_receives`) so retries and failure paths are testable on your machine, and `snoozestack publish` creates the managed queue and trigger hosted. **Why use it** Queues exist to break the assumption that work has to finish while someone is waiting. Without one, every slow or failure-prone step — charging a card, transcoding a video, calling a flaky third-party API — sits inside the user's request. The request is only as fast as its slowest dependency, and if that dependency is down, the user sees the error. Putting a message on a queue instead splits one risky operation into two reliable ones: an *enqueue* that is fast and almost never fails, and a *consume* that can take as long as it needs and retry on its own. That buys you three things — a responsive request path, a buffer that absorbs traffic spikes instead of collapsing under them, and automatic retries, since a message stays on the queue until something successfully handles it. The cost is **eventual consistency**: the work is promised, not done. If the caller needs the answer in the response, don't use a queue — call the function directly. - **Receiving third-party webhooks** — Stripe, GitHub, and Twilio all retry — and eventually give up — if your endpoint is slow. The inbound webhook URL below writes straight to the queue with no project compute in the path, so you accept in milliseconds and process afterwards. - **Slow work triggered by a user action** — Image resizing, PDF generation, sending a welcome email. Insert the row, enqueue the job, return immediately — the user isn't waiting on an email provider. - **Smoothing out spikes** — A launch or a cron-driven stampede produces more work per second than your downstream can take. The queue holds the backlog and the consumer drains it at a steady rate, instead of every request timing out at once. - **Fanning one event out to several handlers** — One “order placed” message can run a receipt email, a warehouse sync, and an analytics roll-up — a queue's `functions` list can name several, and each gets every message. ### Declaring a queue ```bash title="snoozestack/project.toml" [[queues]] name = "jobs" functions = ["handle-payment"] # the consumer(s) — every message goes to each max_receives = 3 # attempts before a message dead-letters ``` ```bash title="terminal" snoozestack dev # queues run locally, in .snoozestack/queues.db snoozestack queues send jobs '{"kind": "resize", "id": 42}' # smoke test snoozestack queues list # jobs messages=1 in_flight=0 delayed=0 ``` `snoozestack publish` creates the hosted queue and its trigger, and `snoozestack preview` your preview's. A declaration only ever creates or updates — removing a `[[queues]]` entry never silently destroys a hosted queue; `queues delete` is the only removal, and it prompts. Each namespace has its own queues, so a preview's backlog never leaks into live. **Locally**, `snoozestack dev` keeps the queue in `.snoozestack/queues.db` and runs it the way the managed queue runs: a `POST /_queues/` answers `202` at once (add `?delay=` for a delayed message), a drain tick every 10 seconds delivers what is due to each declared function, a failed delivery becomes visible again after a 30-second visibility window with its receive count one higher, and the `max_receives`th failure moves it to the dead-letter list — the same sequence a consumer meets hosted, so one that is not idempotent, or that assumes delivery is synchronous, is found out here. `GET /_queues/` and the Queues page show the depth (messages · in flight · delayed), recent deliveries and dead letters. A test that must not wait can switch to inline delivery: ```bash title="snoozestack/dev.toml" [dev] queues = "immediate" # a POST runs the consumer inline and returns every attempt's outcome (or: snoozestack dev --queues=immediate) queue_tick_ms = 500 # the drain cadence in async mode (default 10000) queue_visibility_ms = 500 # how long a failed delivery stays invisible (default 30000) ``` | Surface | Availability | | --- | --- | | Portal UI | Each declared queue with the functions it dispatches to, its hosted depth (messages · in flight · delayed), recent deliveries and dead-letter count; a Triggers card with each trigger's last run; the webhooks feeding it; Notifications for push-sending triggers | | CLI | `snoozestack queues list/push/send/dead-letters/delete`, `triggers …`, `webhooks …` — each scoped by `--dev`/`--preview` | | SDK / HTTP | `snoozestack.queues.send(queue, message)` from trusted server code, or `POST /api/queues/v1/` | **Diagram:** External sender · Stripe, GitHub, … · POST · .snzzz.com · /api/webhooks/ · write only · Your producers · SDK, CLI, functions · send · Queue · [[queues]] · trigger · Linked function(s) · all must succeed to ack · max_receives exceeded · Dead letters · inspect · redrive ### Sending a message Enqueueing is a trusted operation — a queued message can become work, or a [push notification](https://snoozestack.com/docs/push-notifications.md), so browsers and app binaries don't get to do it directly. Send from your own functions or server-side code, or let a webhook write inbound traffic straight onto the queue. ```bash title="from your own server" import { createClient } from "snoozestack-js"; const snoozestack = createClient(process.env.SNOOZESTACK_URL, process.env.SNOOZESTACK_API_KEY); await snoozestack.queues.send("jobs", { kind: "booking-confirmed", id: 42 }); ``` ### Triggers A **trigger** is the link between a queue and what happens to its messages. Declaring `functions` on the `[[queues]]` entry is the usual way to get one — publish creates it alongside the queue. The standalone `snoozestack triggers` commands cover the rest: linking a queue to a **push notification service** (`--notify`), pausing without deleting (`disable`), and one-off wiring outside the file. A trigger fires on *any* message the queue receives — webhook, SDK send, or CLI — and a message is only removed once *every* destination succeeded. ### Webhooks A **webhook** gives a queue a public inbound URL so outside systems can put messages on it — nothing more. It runs no functions on its own; the queue's trigger processes what it delivers. ```bash title="snoozestack/project.toml" [[webhooks]] name = "stripe-events" queue = "jobs" ``` ```bash title="terminal" snoozestack publish snoozestack webhooks list # stripe-events → https://.snzzz.com/api/webhooks/stripe-events/ ``` The URL is on your project's own host, and the write path runs on managed queue infrastructure with no project compute in it — it keeps accepting even while you redeploy. The unguessable `` in the URL is the credential; point Stripe or GitHub at it directly. Locally, `snoozestack dev` serves the same webhook at `POST /_webhooks/`, enqueueing into the same local queue (and answering `202`, as the hosted URL does) so the body retries and dead-letters exactly as hosted. ### Writing the consumer A triggered function receives the message's raw body as its request body — the same `(req, capabilities)` contract as every function. The response is the contract that matters: **2xx means “done, delete it”**, anything else means “retry later.” Since a retry can redeliver a message you already partly handled, make the handler **idempotent** — key the work on something stable from the payload so processing it twice is harmless. ```ts title="snoozestack/functions/handle-payment/index.ts" export default async function handler(req: Request, capabilities) { const event = await req.json(); // Idempotency: the provider's own event id is the natural key. A unique // index on event_id plus "on conflict do nothing" turns a redelivery into // a no-op instead of a double charge. try { capabilities.db.query( "insert into payments (event_id, amount) values (?, ?) on conflict (event_id) do nothing", [event.id, event.amount], ); } catch (err) { // Non-2xx leaves the message on the queue for redelivery, so a // transient blip fixes itself. console.error("payment upsert failed", err); return new Response("retry", { status: 500 }); } return new Response("ok"); // 2xx → message deleted } ``` ### Dead letters A message that keeps failing is redelivered up to `max_receives` times (default 3 — hosted and in `snoozestack dev`'s local queue alike, each redelivery after the visibility window) and then moves to the queue's **dead-letter queue** instead of retrying forever. `snoozestack queues dead-letters ` lists what's sitting there — the same view as the console and, locally, `GET /_queues/` — so a permanently bad payload is something you inspect and fix, not an invisible retry loop. --- # Schedules > Cron-triggered runs of a declared function — versioned with the code, testable locally. [HTML version](https://snoozestack.com/docs/schedules/) · [Docs index](https://snoozestack.com/llms.txt) A schedule runs one of your [functions](https://snoozestack.com/docs/functions.md) on a cron timer. It is declared in `project.toml` next to the function it calls, so the timing, the time zone, and the payload are versioned with the code they trigger — and `snoozestack dev` fires them locally, which means a recurring job is something you can watch work before it is ever published. **Why use it** Some work has no request to hang off: a nightly digest, a weekly roll-up, an hourly sync with someone else's API, a cleanup that reclaims what expired. Without a scheduler that work ends up in a cron entry on a machine somebody has to remember, or in a task queue no one reads — either way it is outside the project, and it drifts. Declaring it beside the function keeps the whole job in one reviewable place: a pull request that changes when the digest runs looks exactly like a pull request that changes what it says. And because the timer targets a function rather than a shell command, everything the rest of the platform gives that function — [declared capabilities](https://snoozestack.com/docs/permissions.md), the same local runtime, its own signals — applies unchanged. ### Declaring one ```bash title="snoozestack/project.toml" [[schedules]] name = "nightly-report" function = "report" # a declared [[functions]] entry cron = "0 3 * * *" # minute hour day month weekday timezone = "America/Phoenix" # optional; UTC when omitted payload = '{"period": "daily"}' enabled = true ``` | Field | Meaning | | --- | --- | | `function` | The declared function to invoke — validation fails if no such function exists | | `cron` | Five fields: minute hour day month weekday | | `timezone` | An IANA zone, so a 9am job stays 9am across daylight saving | | `payload` | JSON delivered as the request body, for a function that serves several schedules | | `enabled` | Set false to keep the declaration but stop it firing | `snoozestack check` validates the expression and the target before anything runs, so a malformed cron field or a typo'd function name is a build error rather than a job that silently never fires. | Surface | Availability | | --- | --- | | Portal UI | Read-only — the console lists a project's schedules; the declaration owns them | | CLI | `snoozestack check` validates, `snoozestack publish` ships; `functions schedules …` covers one-off changes | | SDK / HTTP | None — a schedule is infrastructure, not something app code turns on at runtime | ### Running it locally `snoozestack dev` honours declared schedules, so the recurring path is exercised on your machine the same way the request path is. For a job that only runs at 3am, don't wait for it — invoke the function directly, since a scheduled run is an ordinary call with the payload as its body: ```bash title="terminal" snoozestack dev curl -X POST localhost:8787/report -d '{"period":"daily"}' # the same thing the timer does snoozestack signals tail --type log # watch what it did ``` ### Writing the function ```ts title="snoozestack/functions/report/index.ts" export default async function handler(req: Request, capabilities) { const { period } = await req.json().catch(() => ({})); // the declared payload const { rows } = capabilities.db.query( "select count(*) as rounds from rounds where played_at >= date('now', '-1 day')", ); capabilities.signals.emit("event", "digest_sent", { period, rounds: rows[0][0] }); return new Response("ok"); } ``` Two habits worth keeping. Make the handler **idempotent** — a retry, or an operator running it by hand, shouldn't double-send. And have it **say what it did** through [Signals](https://snoozestack.com/docs/signals.md): nobody is watching at 3am, so the emitted event is the only evidence the run happened and what it decided. - **Digests and summaries** — The weekly round-up email, the Monday report. One timer, one function, and the copy it sends lives in the same commit as the schedule. - **Syncing an external system** — Pull yesterday's records from someone else's API on an hourly timer. Pair it with a [queue](https://snoozestack.com/docs/queues.md) when the work per run is large — the schedule enqueues, the consumer drains. - **Housekeeping** — Expiring invitations, pruning stale drafts, recomputing a leaderboard. Cheap work that has to happen on a rhythm rather than on a request. ### Hosted behaviour `snoozestack publish` creates the declared schedules on the managed clock. Each namespace keeps its own — a preview's timers fire against that preview's data and never against live. A run is an ordinary function invocation, so its output lands in [logs](https://snoozestack.com/docs/logs.md) alongside everything else the function does, and a failure is visible there rather than being swallowed by the timer. --- # Authentication > Application identity: magic link, Google, Apple, and email/password — declared in the project, verified by the runtime. [HTML version](https://snoozestack.com/docs/authentication/) · [Docs index](https://snoozestack.com/llms.txt) Every project gets application identity — sign-in for *your app's own users* — with four methods: **magic link** (on by default), **email/password**, and **Google** and **Apple** sign-in. What the project offers is declared in `project.toml`'s `[auth]` section; hosted, the sign-in flows and session verification both run inside your project's [runtime](https://snoozestack.com/docs/runtime.md) — locally, per request, no round trip — before your [function](https://snoozestack.com/docs/functions.md) sees it as `capabilities.auth.user`. | Surface | Availability | | --- | --- | | Portal UI | **Users** (delete/ban) and **Providers** (declared methods + the one callback URL to register) — configuration itself is files | | CLI | `snoozestack publish` sends the declared config (a preview runs the checkout's); `auth pull` writes the email/page templates out as files | | SDK / HTTP | `snoozestack.identity.*` — magic link, password, OAuth, id-token, sessions, sign-out | **Diagram:** Client · magic link · OAuth · password · sign in · Runtime identity · …/auth/… · auth.db · session token · Client session · sent with each call · Runtime verifies locally · signature + session row, no round trip · capabilities.auth.user · { id, email } in your function · same process · Sign-in, refresh, verification and revocation all run in the project's runtime; a revoked session fails on its next request. **Why use it** Authentication is the classic thing not to build yourself. Not because the happy path is hard — checking a password hash is twenty lines — but because the rest isn't: secure password storage, session expiry and refresh, reset tokens that can't be replayed, OAuth callback handling, email delivery. Each is a well-known place to get security subtly wrong, and none of it is what makes your product worth using. **Your runtime owns identity.** Each project keeps its users, password hashes, sessions and signing keys in a separate `auth.db`, replicated alongside the application database. Sign-in, refresh and verification run in that container; revoking a session takes effect on its next request. OAuth still uses the stable callback on your project host. The portal relays email and the console's user management requests. The identity database stays separate from your schema, SQL editor and database exports. Put profile fields in your own table keyed by `capabilities.auth.user.id`. Local development and previews keep a built-in `user@test.com` / `1234` account; production never has it. - **Passwordless by default** — Magic link is enabled out of the box: a successful sign-in proves the address and creates the user in one flow — nothing to configure and no password to reset. It doubles as the reset flow when you add passwords later. - **Ordinary email/password** — `signUpWithPassword()` / `signInWithPassword()`. Turn on `confirm_signups` if you need verified addresses; leave it off while prototyping so sign-ups return a session immediately. - **Sign in with Google / Apple** — Lower friction, and Apple is required by App Store review once an iOS app offers another social login. Each needs a one-time registration in the provider's console — walked through below. Native apps skip the browser entirely with `signInWithIdToken`. - **Per-user data without trusting the client** — A function with `auth = "required"` binds `capabilities.auth.user.id` into its own queries — the client never sends, and cannot forge, its identity. See [Permissions](https://snoozestack.com/docs/permissions.md). ### Declaring it ```bash title="snoozestack/project.toml" [auth] redirect_urls = ["https://myapp.example.com/welcome", "myapp://callback"] confirm_signups = true # off by default: on, password sign-ups must confirm by email [[auth.providers]] name = "google" client_id = "1234-abc.apps.googleusercontent.com" secret_name = "GOOGLE_OAUTH_SECRET" # a declared [[secrets]] entry [[auth.providers]] name = "apple" client_id = "com.example.app.signin" # the Services ID secret_name = "APPLE_SIGNIN_KEY" # the .p8 key's contents team_id = "ABCDE12345" key_id = "XYZ9876543" native_client_ids = ["com.example.app"] # iOS bundle ids for id-token sign-in ``` `client_id` is public by design (it rides in every browser redirect); `secret_name` is a *reference* to a declared secret — the value is set with `snoozestack secrets set`, never committed. `redirect_urls` is the allowlist of where sign-in may send a visitor afterwards: your project's own URLs are allowed automatically; add your app's domains, deep links, and localhost here. `snoozestack publish` sends the declaration up, and a preview runs the checkout's own. A preview never needs its URL declared: inside a preview each declared URL's path is moved onto the preview's own address, which is allowed too, and production's list stays exactly as committed. A request from a preview page is answered by that preview even when your code names production's API base, so testing a preview never signs anyone up in production. Provider registrations are **per project** — your users see your app's name on the consent screen, and no other tenant shares your registration. ### Sessions, in your functions ```bash title="app code" await snoozestack.identity.requestMagicLink({ email, redirectTo: "https://myapp.example.com/welcome" }); // … user clicks the emailed link … const { user } = await snoozestack.identity.completeMagicLink(); // From here every functions.invoke() carries the session automatically. const { data } = await snoozestack.functions.invoke("my-notes"); ``` Inside the function, the runtime has already verified the token's signature before your code runs: `capabilities.auth.user` is `{ id, email }` (or `null` under `auth = "optional"` with no session). Sessions are short-lived and refresh automatically in the SDK; `identity.onStateChange()` observes `anonymous | pending | authenticated | expired` reactively. ### Local development: the same plane, on your machine Under `snoozestack dev`, the runtime serves the project's own `/auth/…` routes from the same module the hosted container runs, backed by `.snoozestack/auth.db` on your machine — users, passwords, magic links, OAuth state, sessions with immediate revocation, rate limits, self-delete, and the push device registry, all real. It comes seeded with one account, `user@test.com` / `1234` (id `d0000000-0000-4000-8000-000000000001`) — a real address, so an `` accepts it — and any email can sign up beside it. A preview's URL has the same account, so one login works on both. Only the parts that need the outside world are simulated, at the provider boundary: the Google and Apple buttons open a local “choose an account” page instead of the provider (the seeded account is one click; a new email creates a user; Cancel is the provider's `access_denied`) and then complete the real callback, so `auth.db` gains the same state and subject rows it would hosted. A native app posts an id_token minted by the dev server itself (`GET /_dev/oauth//id-token?email=…`) or the shorthand `dev:`. Email is rendered with your own templates and written to the dashboard's Emails page instead of sent, so a sign-up or magic link completes from there. Nothing leaves your machine, and the dev server logs each simulated hop. Hosted's refusals happen locally too. Every `/auth/…`, `/push/…` and `/signals/v1/…` request needs the project's anon key; locally that is a stable `sb_local_…` key derived from the project name, printed at startup and shown on the Overview, so an app that never sends a key fails on your laptop rather than after publishing. The same per-route rate limits apply. Both can be turned off in `snoozestack/dev.toml` (`[dev] api_key = false`, `rate_limits = false`) for load scripts. Two dev-only routes let a browser or integration test begin clean and check what happened; neither exists on any hosted project, custom domain or preview: ```bash title="terminal" curl http://localhost:8787/_dev/auth # { mocked: true, apikey, users, seeded_user, last_sign_in: { provider, at } | null, sessions, pending_oauth, outbox } curl -X POST http://localhost:8787/_dev/auth/reset # every user, session, device and outbox message gone; the seeded account is back snoozestack dev reset # also removes auth.db and the outboxes on disk; the next start re-seeds ``` ### Email & page templates are files The three templates a project owns — the confirm-signup email, the sign-in-link (magic link) email, and the reset-password page — live in the repo, so they diff, review, and roll back like everything else: ```bash title="terminal" snoozestack auth pull # writes the current templates into the project # snoozestack/authentication/email/confirm-signup.html # snoozestack/authentication/email/sign-in-link.html # snoozestack/authentication/page/reset-password.html ``` The two emails carry their subject on the first line as an HTML comment (``), so the file stays a fragment you can open in a browser. `snoozestack publish` sends whichever exist with the release; the console shows them read-only. There is no template editor UI, on purpose: the files are the one copy. ### Sign in with Google A one-time setup in the Google Cloud console, about ten minutes. You are creating an OAuth client at Google, then declaring its credentials in the project. The callback URL to register is shown on the console's **Authentication → Providers** page: `https://.snzzz.com/auth/oauth/google/callback` — there is exactly one, and it must be registered character-for-character. 1. **Create or pick a Google Cloud project** at [console.cloud.google.com](https://console.cloud.google.com) — the project dropdown in the top bar → **New Project**. 1. **Configure the OAuth consent screen.** Under **APIs & Services → OAuth consent screen** (newer consoles: **Google Auth Platform → Branding**), click **Get started**, fill in the app name and support email, pick **External** as the audience (or **Internal** on Workspace for org-only sign-in), and **Create**. 1. **Add the scopes under Data Access**: `openid`, `…/auth/userinfo.email`, and `…/auth/userinfo.profile` — then **Update** and **Save**. 1. **Create the OAuth client** on the **Clients** tab: application type **Web application**, with your project's callback URL under **Authorized redirect URIs**. Save the client ID and secret — the secret can't be viewed again. (An iOS app additionally gets its own **iOS**-type client — client ID only — for native sign-in, below.) 1. **Declare it in the project.** Add the `[[auth.providers]]` entry with the web client's id, set the secret, and publish: ```bash title="terminal" snoozestack secrets set GOOGLE_OAUTH_SECRET=GOCSPX-… snoozestack publish ``` 1. **Add yourself as a test user** under **Audience → Test users** — while the Google app is in **Testing**, only listed accounts can complete sign-in. 1. **Test the flow** from your app: ```bash title="sign in with Google" snoozestack.identity.startOAuth({ provider: "google", redirectTo: "https://myapp.example.com/welcome", // must be on redirect_urls }); // … back on that page: const { user } = await snoozestack.identity.completeOAuthCallback(); ``` 1. **Publish the Google app** (Audience → **Publish app**) — with only these three non-sensitive scopes it goes live immediately, and any Google account can sign in from then on. | Error | Cause | | --- | --- | | `redirect_uri_mismatch` | What you registered when creating the OAuth client isn't character-for-character the project's callback URL. | | `access_blocked` | The consent screen is still in **Testing** and the account isn't on its test-user list — add it under **Audience**, or publish the app. | ### Sign in with Apple Same shape as Google with one difference worth knowing up front: Apple issues no static client secret. The secret is a token signed with a `.p8` key you download once — the runtime signs it for you from the four values the provider entry declares (Services ID, Team ID, Key ID, and the key itself). Everything on Apple's side happens under [**Certificates, Identifiers & Profiles**](https://developer.apple.com/account/resources/identifiers/list): | Value | Where it comes from | Lands in | | --- | --- | --- | | **App ID / Bundle ID** | Identifiers → an **App IDs** entry with Sign in with Apple ticked | `native_client_ids`, for a native app | | **Services ID** | Identifiers → a **Services IDs** entry (convention: `com.example.app.signin`) | `client_id` | | **Team ID** | Top right of the developer account | `team_id` | | **Key ID + .p8 key** | Keys → a **Sign in with Apple** key — downloadable exactly once | `key_id`, and the key contents via `snoozestack secrets set` | 1. **Register the Services ID and point it at your project.** Open the services ID, tick **Sign in with Apple** → **Configure**: primary App ID from above; **Domains** is `.snzzz.com` (host only); **Return URLs** is the callback from the console's Providers page — `https://.snzzz.com/auth/oauth/apple/callback`. The final **Save** on the identifier page is easy to miss, and without it the configuration is discarded. Skipping the Return URL is the most common way Apple sign-in fails, and the error says so only if you know the phrasing: Apple's page answers `"Invalid web redirect url."` with `errorCode: invalid_request` rather than naming the URL. A native app signing in with an id_token needs no Return URL at all — only the browser redirect flow does. 1. **Create the signing key** under **Keys**: tick **Sign in with Apple**, configure it against the same App ID, register, note the **Key ID**, and download `AuthKey_.p8` — **Apple lets you download it exactly once**; a lost key can only be revoked and replaced. 1. **Declare it and publish:** ```bash title="terminal" snoozestack secrets set APPLE_SIGNIN_KEY="$(cat AuthKey_XYZ9876543.p8)" snoozestack publish ``` 1. **Test the flow.** On the web this is the ordinary redirect (`startOAuth({ provider: "apple" })`); on iOS, pass the credential from `ASAuthorizationController` straight to an id-token sign-in so the user never leaves the app: ```swift title="iOS (Swift)" let result = try await snoozestack.identity.signInWithIdToken( provider: "apple", idToken: idToken, nonce: nonce ) ``` On iOS, prefer `signInWithIdTokenResolvingPrivateRelay` over plain `signInWithIdToken` when users can pick **Hide My Email**: Apple puts the relay address in the identity token on every sign-in but drops it from the credential object after the first authorization, so a user whose record was created without it comes back with `user.email == nil` from then on. The variant reads the token's claim when the session has none and remembers it on the device. | Error | Cause | | --- | --- | | `invalid_client` | The declared Services ID, Team ID, Key ID, and key don't agree — re-check the four values, then publish again. | | `invalid_request` | The Return URL isn't registered character-for-character on the services ID, or its domain hasn't been verified by Apple. | | `bad ID token from a native app` | The app's bundle ID isn't in `native_client_ids`, or the nonce it was issued with isn't the one passed to `signInWithIdToken`. | ### Native id-token sign-in (Google too) Both Google and Apple mint an id_token a native app can present directly to `POST /auth/oauth//id-token` — no browser round trip, no redirect URL. Declare the native client id (Google: the iOS client's id; Apple: the bundle ID) in `native_client_ids`, and use the SDKs' `signInWithIdToken`. The response includes `isNewUser`, for routing first-time sign-ins to onboarding. ### Users The console's **Authentication → Users** page lists the project's signed-up users with the sign-in method each account used, and can **ban/unban** or **delete** one. Accounts are created by people signing up — there is deliberately no “add a user with a password you typed” button. Each namespace has its own user population: a preview's users are not live's. ### HTTP API (/api/auth) ```bash title="application identity — on the project's own host" # Request a magic link curl -X POST "https://.snzzz.com/api/auth/magic-link" \ -H "Content-Type: application/json" \ -d '{"email": "me@example.com", "redirect_to": "https://myapp.example.com/welcome"}' # Password sign-in → { session_token, refresh_token, user } curl -X POST "https://.snzzz.com/api/auth/password/sign-in" \ -H "Content-Type: application/json" \ -d '{"email": "me@example.com", "password": "secret123"}' # The project's public signing keys — verify a session token in your own backend curl "https://.snzzz.com/api/auth/jwks" ``` Also under `/api/auth`: `password/sign-up`, `password/change`, `magic-link/verify`, `session` (refresh), `sign-out`, `delete` (a signed-in user removes their own account — remove the app's own data about them first), `oauth//start|callback|id-token`, and the hosted `reset` page. Password reset needs no second system — the reset flow *is* the magic link. The provider *callback* URLs registered at Google and Apple are the one exception without the `/api` prefix (`…snoozestack.com/auth/oauth//callback`) — deliberately, since a registration must keep resolving permanently. See [snoozestack-js](https://snoozestack.com/docs/sdk-js.md) for the same operations through `snoozestack.identity`. --- # Permissions > Auth modes gate entry, declared capabilities gate reach, and your query decides the rest. [HTML version](https://snoozestack.com/docs/permissions/) · [Docs index](https://snoozestack.com/llms.txt) Access control has three layers here, and all three are visible in your repo. A function's `auth` mode decides *who gets in*; its declared `capabilities` decide *what the code can reach*; and the query it writes decides *which rows this caller may touch*. Nothing else is in play — no client-facing table API, no ambient credentials, no policy engine applying itself behind your back. | Layer | Declared where | What it gates | | --- | --- | --- | | `auth = "public" | "optional" | "required"` | project.toml, per function | Whether a request needs a verified session to reach the handler at all | | `capabilities = ["db", …]` | project.toml, per function | Which surfaces exist on the handler's second argument — an undeclared one simply isn't there | | The query itself | The function's code | Row-level rules — `where user_id = ?` with `capabilities.auth.user.id` | **Diagram:** Anonymous request · no session · Signed-in request · verified session · public / optional · Function · auth = "public" | "optional" | "required" · declared only · Capabilities · db · storage · push · … · Who may do what is the query the function writes — where user_id = capabilities.auth.user.id — · the same check a hand-written API handler always made, in code you review. **Why use it** If your app talked to the database directly from a browser or a phone, something would have to stop one user from reading another's rows — and it couldn't be the client, because the client is the thing you don't trust. SnoozeStack removes that path rather than policing it: a committed function is the only way to your data, and a function is code you write and can read — the same discipline as an API handler, minus the server to run. The capability declaration puts the blast radius in the diff. A reviewer can see that `send-digest` can read the database and send push but cannot touch storage, and a compromised or buggy function can't reach what it never declared. Secrets go further: a `[[secrets]]` entry can name exactly which functions may read it. The trade-off is that the row-level check is written by hand, every time. Forgetting the `where user_id = …` in a function that reads private data is a real bug, the same way forgetting it in a traditional API handler always was. Write the check where you write the query, and keep each function's job narrow. ### The auth modes ```bash title="snoozestack/project.toml" [[functions]] name = "my-notes" entry = "my-notes/index.ts" auth = "required" # no valid session → 401, handler never runs capabilities = ["db", "auth"] [[functions]] name = "public-feed" entry = "public-feed/index.ts" auth = "public" # anyone; capabilities.auth is absent capabilities = ["db"] ``` `required` rejects unauthenticated requests before your code runs, so the handler can rely on `capabilities.auth.user` being set. `optional` runs either way and tells you which (`capabilities.auth.anonymous`) — for endpoints that personalize when signed in. `public` is for webhooks and reads that are the same for everyone. Sessions are verified by the runtime itself — a signature check, local and offline — so this gate doesn't cost a network round trip; see [Authentication](https://snoozestack.com/docs/authentication.md). - **Private per-user data** — Notes, orders, messages: `auth = "required"`, and every query the function runs carries `where user_id = ?` bound to `capabilities.auth.user.id`. The default shape, and the one most apps need. - **Public read, owner write** — Blog posts, product listings: a `public` read function serves anyone; a separate `required` write function checks the caller before it inserts or updates. - **Shared workspaces** — Multi-tenant B2B, where access follows membership rather than ownership — the function's query joins through a `memberships` table to check the caller belongs to the row's team, in the same SQL statement that reads the row. - **Admin and background work** — Back-office tools and [queue](https://snoozestack.com/docs/queues.md)/schedule-triggered jobs that legitimately see everything are themselves functions — there's no separate bypass credential to protect; they simply don't add a caller check. | Surface | Availability | | --- | --- | | Portal UI | Read-only — the console shows each function's auth mode and capabilities; changing them is a project.toml edit | | CLI | `snoozestack check` validates the declarations; `snoozestack publish` ships them | | SDK / HTTP | The SDK attaches the signed-in session to `functions.invoke()` automatically; the function decides the rest | ### Access through membership Team and workspace apps need “you can see this row if you belong to its team,” which is a join in the function's own query rather than a separate policy to maintain: ```ts title="snoozestack/functions/team-documents/index.ts" const { rows } = capabilities.db.query( `select d.* from documents d where d.team_id = ? and exists (select 1 from memberships m where m.team_id = d.team_id and m.user_id = ?)`, [teamId, capabilities.auth.user.id], ); ``` Index what the join compares, the same as you would for any query that runs often. ### Common gotchas - `auth = "required"` only proves *someone* is signed in — it doesn't scope a query for you. A per-caller function still binds `capabilities.auth.user.id` into its SQL itself. - There is no fail-closed default at the row level — a `db` function that forgets the `where` clause returns every row to whoever got in. Prefer narrow functions over a general-purpose one that re-derives the rule for every caller. - Declaring a capability a function doesn't use widens its blast radius for nothing; declare what it needs, no more. Same for secrets — use `[[secrets]] functions = […]` to name the functions that may read each one. - The project key your app initializes the SDK with identifies the *project*, not a person — treat it as public, and never treat its presence as authorization. --- # Secrets > Declare secret names in the project; set their values with the CLI, per namespace. [HTML version](https://snoozestack.com/docs/secrets/) · [Docs index](https://snoozestack.com/llms.txt) A secret is a credential your [functions](https://snoozestack.com/docs/functions.md) need but your repository must never hold — a third-party API key, a signing key, a webhook token. The **name** is declared in `project.toml` (and can name exactly which functions may read it); the **value** is set with the CLI and stored by the platform, never committed. **Why use it** Splitting the name from the value is what makes a credential reviewable. The diff shows that a new function wants `STRIPE_SECRET_KEY` and that only `checkout` may read it; the value itself never appears in a pull request, a log line, or the console. A reviewer can reason about blast radius without ever seeing the thing they're protecting. It also means a rotation is not a deploy. Values live outside the immutable version your project publishes to, so setting a new one takes effect on the running runtimes without rebuilding or republishing anything. ### Declaring one ```bash title="snoozestack/project.toml" [[secrets]] name = "OPENAI_API_KEY" functions = ["chat"] # omit to let every function read it description = "Model access for the caddie assistant" [[secrets]] name = "STRIPE_WEBHOOK_SECRET" optional = true # publish can proceed before you have it ``` Names follow environment-variable rules (letters, digits, underscores). `functions` is the allowlist — a function not named there has no such property on its `capabilities.secrets`, so a leak can't come from code that was never supposed to hold the credential. `optional = true` lets `snoozestack publish` succeed while a credential is still being obtained, instead of blocking the whole release on one pending key. ### Setting values ```bash title="terminal" snoozestack secrets set OPENAI_API_KEY=sk-… # the hosted default snoozestack secrets set OPENAI_API_KEY=sk-test… --dev # local-only override snoozestack secrets set OPENAI_API_KEY=sk-… --preview staging snoozestack secrets list # names + which namespaces override snoozestack secrets unset OPENAI_API_KEY ``` | Namespace | Set with | Used by | | --- | --- | --- | | `dev` | `--dev` | Your machine, under `snoozestack dev` — stored in `.snoozestack/secrets.json`, never sent anywhere | | a preview | `--preview ` | That preview only; falls back to the default when unset | | `default` | no flag | Live, and any preview without its own override | `--dev` is local-first: `secrets set --dev` writes `.snoozestack/secrets.json` (mode 0600, gitignored) with no account and no network, and `snoozestack dev` reads it at startup and on every `project.toml` reload. A declared name missing from the file is fetched from the linked project's `dev` namespace, when the directory is linked and logged in, and cached into the file so the next start needs nothing; unlinked, it simply reads as unset and the startup line says which name and how to set it. The Secrets page shows where each loaded value came from. `snoozestack dev reset` leaves the file alone — it is your input, not derived state. `secrets list` prints names and which namespaces override them — never values. A value cannot be read back out of the platform by any surface: not the CLI, not the console, not the management API. If you lose one, rotate it at the provider and set the new value. | Surface | Availability | | --- | --- | | Portal UI | Read-only: declared names and which namespaces override them, never values | | CLI | `snoozestack secrets set/list/unset`, scoped by `--dev` / `--preview` | | SDK / HTTP | None, deliberately — a secret that reached a client wouldn't be one | ### Reading one in a function ```ts title="snoozestack/functions/chat/index.ts" export default async function handler(req: Request, capabilities) { const key = capabilities.secrets.OPENAI_API_KEY; // declared for this function // … call the vendor with it; the value never leaves the runtime } ``` The function must also declare the `secrets` capability — see [Permissions](https://snoozestack.com/docs/permissions.md). Changing a value reaches the runtimes already running, so a rotation needs no republish; what a container cannot pick up without being recreated is a change to its *declaration*. - **A third-party API key** — The model, payment, or email provider your function calls. Name it for the one function that calls it, and nothing else in the project can reach it. - **Provider credentials for sign-in** — An [auth provider](https://snoozestack.com/docs/authentication.md)'s client secret or signing key. The provider entry references the secret by name — `secret_name` — so the key itself stays out of the file. - **A different value per namespace** — Test credentials under `--dev` and a preview, the real ones on live, with the same code and the same declared name throughout. --- # snoozestack-js — JavaScript & TypeScript > The JavaScript/TypeScript client SDK for snoozestack. [HTML version](https://snoozestack.com/docs/sdk-js/) · [Docs index](https://snoozestack.com/llms.txt) snoozestack-js v1.14.0 · [Install or upgrade](https://snoozestack.com/docs/installation.md) `snoozestack-js` is a dependency-free client for a project's identity, functions, queues, and Signals — plus the `defineTable()` schema vocabulary the runtime builds your database from. Install it from [snoozestack.com/js](https://snoozestack.com/docs/installation.md). ```bash title="setup" import { createClient } from 'snoozestack-js'; const snoozestack = createClient( 'https://.snzzz.com/api', // Project Overview SNOOZESTACK_ANON_KEY, // the public project key ); ``` Every service hangs off that `/api` base — `/api/auth`, `/api/functions/v1`, `/api/storage/v1` and the rest — and a [custom domain](https://snoozestack.com/docs/domains.md) serves the identical paths under its own name. In a browser app you can pass the base on its own: ```bash title="browser app served by snoozestack" // Same-origin: resolves against whichever host serves the page, so the same // build works on .snzzz.com and on a custom domain. const snoozestack = createClient('/api', SNOOZESTACK_ANON_KEY); ``` A relative base needs a browser to resolve against, so on a server pass the absolute`/api` URL instead. See [Websites](https://snoozestack.com/docs/websites.md) for publishing the app itself. ### Database There's no query builder on this client, deliberately. A table is only reachable from inside a [function](https://snoozestack.com/docs/functions.md), through its declared `db` capability — the client calls the function instead: ```bash const { data: clubs, error } = await snoozestack.functions.invoke('list-clubs'); await snoozestack.functions.invoke('add-club', { body: { name: '7 iron', distance: 165 }, }); ``` ```ts title="snoozestack/functions/list-clubs/index.ts" export default async function handler(req: Request, capabilities) { const { rows } = capabilities.db.query( "select id, name, distance from clubs where in_bag = 1 order by distance desc limit 20", ); return Response.json(rows); } ``` The schema itself is also this package's job — `defineTable()` and `schema.*` in your committed `schema.mjs` are imported from `snoozestack-js`. See [Schema](https://snoozestack.com/docs/schema.md) and [Permissions](https://snoozestack.com/docs/permissions.md). ### Application identity Projects on the runtime use `SnoozeStackIdentity` instead: a small state machine (`anonymous` → `pending` → `authenticated` → `expired`) over the project's own `/auth` endpoints, with sessions verified locally by the runtime. All the sign-in methods share it — magic link, Google, Apple, email/password, and native id_token for apps that never see a browser redirect. ```bash import { SnoozeStackIdentity } from 'snoozestack-js'; const identity = new SnoozeStackIdentity(PROJECT_URL, ANON_KEY); // Magic link: request, then complete on the emailed link's landing page await identity.requestMagicLink({ email, redirectTo: 'https://app.example.com/' }); const { user } = await identity.completeMagicLink(); // reads ?token=… itself // Google / Apple: one call each — the provider name is the only difference. // Both navigate the browser to the provider and come back to redirectTo. identity.startOAuth({ provider: 'google', redirectTo: 'https://app.example.com/' }); identity.startOAuth({ provider: 'apple', redirectTo: 'https://app.example.com/' }); const oauth = await identity.completeOAuthCallback(); // on the redirectTo page // Email/password: sign-up stores the password pending until the email is // proven (the standard verification email goes out) — so nobody can squat // an address they don't own. Sign-in is immediate once a password exists. await identity.signUpWithPassword({ email, password, redirectTo: 'https://app.example.com/' }); const signedIn = await identity.signInWithPassword({ email, password }); // A magic-link/OAuth-only user sets a first password by omitting currentPassword. await identity.changePassword({ currentPassword, newPassword }); // Self-serve deletion: remove your own data about the user first, then this // deletes the identity and signs out locally. await identity.deleteAccount(); // Native apps skip the redirect entirely: Google's SDK and Sign in with // Apple hand you a signed id_token, so there is no callback to complete. // nonce is the RAW value you generated, never its SHA-256 — Apple puts the // hash in the token and the server compares the two. await identity.signInWithIdToken({ provider: 'apple', idToken, nonce }); await identity.signInWithIdToken({ provider: 'google', idToken }); identity.onStateChange((state, session, user) => { /* re-render */ }); await identity.signOut(); ``` Sessions refresh themselves before expiry and persist in `localStorage`, namespaced per project and per namespace. Failures reject with a stable `code` (`invalid_credentials`, `rate_limited`, `password_too_weak`, …) on `SnoozeStackIdentityError`. Google and Apple need the project's own provider credentials declared in `project.toml` — see [Authentication](https://snoozestack.com/docs/authentication.md) for the per-provider setup, including the Apple Services ID walkthrough. Against `snoozestack dev` there is a built-in account so you can reach a signed-in screen with no provider set up and no email sent: sign in as `user@test.com` with the password `1234` (a preview has it too) — and any other email can sign up, sign in with a link from the dashboard's Emails page, or use the Google and Apple buttons, which open a local account chooser instead of the provider and then complete the real callback. The client presents the dev server's local anon key (printed at startup) the way it presents the hosted one; the code you write is the code that runs in production. `GET /_dev/auth` and `POST /_dev/auth/reset` let a test inspect and reset the local plane. The seeded account and the `/_dev` routes exist only in the local dev server; nothing hosted — project host, custom domain, or preview — has them. ### Storage The SDK ships no storage client. Uploads and downloads are mediated by your own functions, which keeps the authorization decision in code you wrote rather than in a policy evaluated by a service the client talks to directly. ```bash // Ask one of your functions to authorize a path — it returns a presigned URL. const { data: signed, error } = await snoozestack.functions.invoke('avatar-upload-url', { body: { contentType: file.type }, }); // A presigned URL carries its own signature, so this is a plain PUT. await fetch(signed.url, { method: 'PUT', body: file, headers: { 'content-type': file.type } }); // A public bucket needs no call at all — the object URL is stable: const src = `${SNOOZESTACK_URL}/storage/v1/object/public/avatars/${path}`; ``` See [Storage](https://snoozestack.com/docs/storage.md) for buckets, sharing files, and access control. ### Functions ```bash // POST by default. The signed-in session is attached automatically. const { data, error } = await snoozestack.functions.invoke('chat', { body: { messages }, }); // A reading function is a GET. const { data: wallets } = await snoozestack.functions.invoke('wallets', { method: 'GET' }); ``` An error is *returned*, not thrown — check `error` before using `data`. `error.message` carries the function's own message when it sent one. ### Signals ```bash snoozestack.signals.identify(user.id); // merges their anonymous history in snoozestack.signals.track('signup', { plan: 'pro' }); // batched; never blocks, never throws snoozestack.signals.reset(); // on sign-out ``` Also usable standalone as `SnoozeStackSignals`, with no other client needed. See [Signals](https://snoozestack.com/docs/signals.md) for batching, identity, and the export API. ### Queues ```bash title="server-side only" await snoozestack.queues.send('jobs', { kind: 'booking-confirmed', id: 42 }); ``` Enqueueing is trusted — see [Queues](https://snoozestack.com/docs/queues.md) for why a browser never does it directly. ### Environment variable conventions ```bash title=".env" SNOOZESTACK_URL=https://.snzzz.com/api SNOOZESTACK_ANON_KEY=eyJ… # the public project key — safe in browsers ``` In Vite use the `VITE_` prefix (`import.meta.env.VITE_SNOOZESTACK_ANON_KEY`); in Next.js, `NEXT_PUBLIC_` for browser-side values. Realtime subscriptions are not supported. --- # snoozestack-swift — Swift > The Swift client SDK for snoozestack (iOS, macOS, tvOS, watchOS). [HTML version](https://snoozestack.com/docs/sdk-swift/) · [Docs index](https://snoozestack.com/llms.txt) snoozestack-swift v1.13.0 · [Install or upgrade](https://snoozestack.com/docs/installation.md) `snoozestack-swift` is the client for Apple platforms (iOS, macOS, tvOS, watchOS): application identity, functions, push registration, and signals. There is no query builder, by design — a table is reachable only through a committed function (see [Permissions](https://snoozestack.com/docs/permissions.md)). Add it from [snoozestack.com/swift](https://snoozestack.com/docs/installation.md). Two library products — `SnoozeStack` (the full client) and `SnoozeStackSignals` (signals only, a single dependency-free file). ```bash title="setup" import SnoozeStack // Once, at launch (e.g. in your App init). URL + anon key: Project Overview. let snoozestack = SnoozeStack.createClient( url: "https://.snzzz.com", // SNOOZESTACK_URL key: "" ) ``` ### Database A table is only reachable from inside a [function](https://snoozestack.com/docs/functions.md); call it the same way as any other: ```swift struct Club: Decodable { let id: Int; let name: String; let distance: Int } struct ClubsResponse: Decodable { let clubs: [Club] } let response: ClubsResponse = try await snoozestack.functions .invoke("list-clubs") .value ``` ### Application identity `SnoozeStackIdentity` is the auth surface — the same four sign-in methods as [snoozestack-js](https://snoozestack.com/docs/sdk-js.md) (magic link, Google, Apple, email/password) with async/await and Keychain-backed sessions. ```swift let identity = SnoozeStackIdentity(url: projectURL, apiKey: anonKey) // Magic link: request, then complete from the universal link's token try await identity.requestMagicLink(email: email, redirectTo: callbackURL) let result = try await identity.completeMagicLink(token: token) // Google / Apple: open the returned URL, then complete on the callback. // (Apple private-relay addresses come back as the user's email as-is.) let url = identity.startOAuth(provider: "apple", redirectTo: callbackURL) let oauth = try await identity.completeOAuthCallback(url: incomingURL) // Email/password: sign-up stays pending until the verification email is // proven; sign-in is immediate once a password exists. let verificationRequired = try await identity.signUpWithPassword( email: email, password: password, redirectTo: callbackURL) let user = try await identity.signInWithPassword(email: email, password: password) // A magic-link/OAuth-only user sets a first password by passing nil. try await identity.changePassword(currentPassword: current, newPassword: new) // Self-serve deletion (App Store 5.1.1(v)): remove your own data about the // user first, then this deletes the identity and signs out locally. try await identity.deleteAccount() // Native sign-in — no web sheet, no redirect. Google's SDK and // ASAuthorizationController each hand you a signed id_token directly, so // there is nothing for completeOAuthCallback to read. Pass the RAW nonce you // generated, never its SHA-256: Apple puts the hash in the token and the // server compares the two. let appleUser = try await identity.signInWithIdToken( provider: "apple", idToken: appleIDToken, nonce: rawNonce) let googleUser = try await identity.signInWithIdToken( provider: "google", idToken: googleIDToken) ``` ### Functions ```swift // POST by default. The signed-in session is attached automatically. let reply: ChatReply = try await snoozestack.functions .invoke("chat", body: ["messages": messages]) // A reading function is a GET. let wallets: [Wallet] = try await snoozestack.functions.invoke("wallets", method: .get) ``` There is no storage client: uploads go through a function of yours that returns a presigned URL, which the app then PUTs to directly. A public bucket needs no call — the object URL is stable at `/storage/v1/object/public//`. ### Signals Every client has a `signals` handle; events are persisted to disk (so they survive app kills) and flushed on a timer, at batch size, and when the app is backgrounded. See [Signals](https://snoozestack.com/docs/signals.md). ```swift snoozestack.signals.identify(user.id.uuidString) snoozestack.signals.track("signup", properties: ["plan": "pro"]) ``` ### Push notifications ```swift // After registerForRemoteNotifications() hands you the token: try await snoozestack.push.register(deviceToken: token) try await snoozestack.push.unregister() // on sign-out ``` See [Push Notifications](https://snoozestack.com/docs/push-notifications.md) for the APNs setup, environments, and sending. Realtime subscriptions are not supported. --- # Websites > Publish static sites and expose the project API through a custom domain. [HTML version](https://snoozestack.com/docs/websites/) · [Docs index](https://snoozestack.com/llms.txt) Publish any folder of static files — a Vite/Next export, docs, plain HTML — as a website (public, no keys needed). Declare it in `project.toml` and it travels with the project: `snoozestack dev` serves it locally next to the functions it calls, and `snoozestack publish` takes it live with the backend in the same run: ```bash title="snoozestack/project.toml" [[sites]] name = "www" dir = "dist" # path relative to the project root primary = true # serves at the project host's root ``` Every site serves at `https://.snzzz.com/sites//`. One site is the project's **primary site** (`www` by default): it also serves at the root of the project's own host, `https://.snzzz.com/`, so exports built for a domain root work as-is. Move it with `snoozestack sites primary ` or by passing `--primary` on publish. A site served under a `/sites//` path must be built with that base path (for Vite, `base: '/sites//'`), or its root-absolute `/assets/…` links will 404. The primary site serves at the root, so a default root build works there unchanged. A [custom domain](https://snoozestack.com/docs/domains.md) serves both at the same paths, so the base path a site is built with does not depend on which URL it is reached by. ```bash title="terminal" snoozestack preview # every declared [[sites]] entry, at your preview's URL snoozestack publish # every declared [[sites]] entry, live with the rest of the release # sites # site "www": # Publishing ./dist → site "www" (12 files)… # ↑ index.html # ↑ assets/app-3f2c1a.js # Published: 2 uploaded, 10 unchanged, 0 removed ``` `snoozestack publish` records each declared site as a version inside the [release](https://snoozestack.com/docs/releases.md) and makes those exact versions live as it deploys it; `snoozestack preview` serves them from your own preview instead. A site never goes live on its own: there is no separate site push or promote, so a site is always serving what some release shipped, and every version keeps its own preview URL, so a specific build stays reachable after later ones supersede it. Locally there is no publishing at all: `snoozestack dev` serves every declared site's files itself, and the dashboard lists their URLs. To look before anyone else does, run `snoozestack preview`: your preview serves the checkout's sites while production keeps serving what it already serves. Rollback is `snoozestack publish --release ` — the site goes back together with the runtime it shipped with, never out of step with it. A site published here is served from the same host as its project's API, so it can call the API on its own origin — `/api/auth`, `/api/functions/v1`, `/api/storage/v1` and the rest — instead of building an absolute URL: ```ts title="src/snoozestack.js" import { createClient } from 'snoozestack-js'; // No project URL to configure, and nothing to change when the site later // serves from a domain of your own. export const snoozestack = createClient('/api', SNOOZESTACK_ANON_KEY); ``` The anon key still belongs in the build — it identifies the project and is safe in a browser; your functions and storage's own access policies are what protect the data, not the key. Only the URL disappears. That same `/api` works under `snoozestack dev`: each declared site gets its own local port, and `/api/…` on it forwards to the runtime the way the project host does — so a page written this way runs locally and hosted unchanged. The one place with nothing behind `/api` is a framework's *own* dev server (Vite's, say); proxy it there, or keep the absolute `https://.snzzz.com` URL in an environment variable, which works everywhere without a proxy. ```ts title="vite.config.js" export default { server: { proxy: { '/api': { target: 'https://.snzzz.com', changeOrigin: true } }, }, }; ``` **Why use it** A single-page app is, after it's built, just files — HTML, JS, CSS, images. Nothing about serving them needs a running server, and putting one in front of them only adds a process to keep alive, patch, and pay for while it does nothing but hand back bytes that never change. Publishing to object storage instead means the thing serving your frontend has no moving parts to fail: there is no application to crash under load, and traffic spikes are the platform's problem rather than yours. It also keeps the frontend and backend independently deployable — the site talks to your project's API over HTTPS like any other client, so you can ship a UI change without touching the database, or a migration without rebuilding the UI. The constraint is that this is **static hosting only**: no server-side rendering, no Node process, no API routes. Frameworks that can export a static build (Vite, React, Next's static export, Astro, plain HTML) work; anything needing a server at request time belongs in an [function](https://snoozestack.com/docs/functions.md) instead. - **Your app's frontend** — The common case: a Vite or React build published to `www`, talking to the same project's REST, auth, and storage APIs. - **Marketing and docs alongside the app** — Publish several sites from one project — `www` for the landing page, another for docs — each on its own path under the same project. - **Preview builds** — Publish a branch build under its own site name to share a working URL for review, then delete the site when the branch merges. - **Domain-verification files** — Apple App Site Association, `.well-known` files, and search-console tokens are just files in the folder you publish. | Surface | Availability | | --- | --- | | Portal UI | List sites, view versions and promote/roll back, delete, set the primary — no publish/upload button | | CLI | `snoozestack publish` ships every declared site; `sites list/versions/primary/delete` inspect and manage them | | SDK / HTTP | None — publishing is a build-time step, not something an app calls at runtime | **Diagram:** snoozestack publish · syncs the [[sites]] dir · new version · Immutable version · every file, content-hashed · live · .snzzz.com · or your custom domain · every version · preview--… · permanent preview URL · A release points live at its versions — "publish --release" with an older one is the rollback. · "snoozestack preview" serves its versions at the preview's own URL, leaving live untouched. A publish is a **content sync** against the project's site storage (created on first publish): files whose MD5 matches the stored ETag are skipped, changed and new files are uploaded, and files deleted locally are removed remotely. Re-running a publish with no changes is a no-op. - Directory URLs (and the site root) serve that directory's `index.html`; extension-less paths redirect to their trailing-slash form. - Misses render the site's `404.html` if present. - HTML is served `no-cache` so a republish shows immediately; other assets get a 5-minute cache. - Content types come from file extensions (html, css, js, images, fonts, wasm, …). - The bucket is private, and deleting the site (or project) stops serving immediately. - Per-site sizes and file counts are on the Websites page itself; serving activity is under `snoozestack logs sites`, and who published what is in the audit log (`snoozestack audit`, or the console's Settings → Audit log). Connecting a [custom domain](https://snoozestack.com/docs/domains.md) adds a second front door rather than replacing this one. Both serve every site at the same paths — the **primary** one at the root (`example.com/`), every other at `/sites//` — so publishing does not change and no site needs a name of its own. The link on each row points at whichever URL the project is currently handing out; the other keeps working either way. ### The /api base The base URL your apps point at — `https://.snzzz.com/api` — exists as soon as the project does, with nothing to turn on: caching is disabled there and all methods pass through to the project's runtime. A connected domain serves the same paths under its own `/api`, which is why a site published here should use the relative `/api` form — it then stays correct wherever it is served from. --- # Custom domains & HTTPS > Custom domains, HTTPS, and DNS. [HTML version](https://snoozestack.com/docs/domains/) · [Docs index](https://snoozestack.com/llms.txt) Connect a domain you own to a project — declared in `project.toml`, claimed with one command — and snoozestack manages everything behind it: the hosted zone, a free certificate, the DNS records, and the edge distribution that puts the domain in front of the project's websites and API. The console's **Sites → Domains** page shows the same checklist and records. **Why use it** A custom domain is mostly about not being locked to someone else's hostname. The name your users bookmark, the origin your mobile app has pinned in a shipped binary, and the address in your OAuth provider's config are all expensive to change later — putting them on a domain you control means the backend behind them can move without any of it breaking. There is also a concrete unblocking effect. Some things only your own name can do: Google's consent screen says “to continue to” the domain the login runs on, Apple's universal links need a verification file served from your domain, and cookies scoped to your own name are what let a site and its API share a session. The domain itself stays yours — registered wherever you registered it. SnoozeStack only runs the serving infrastructure behind it, so disconnecting a domain from a project leaves the domain itself untouched. - **Your site on your own name** — `example.com` serving your published website, and `example.com/sites//` serving the others. - **One build, either URL** — A connected domain serves the same `/api` paths as `.snzzz.com`, so a browser app that calls `/api/functions/v1/…` on its own origin works from both without a rebuild or an environment variable. - **Environment-per-subdomain** — `staging.example.com` and `app.example.com` on separate projects, sharing one zone and one wildcard certificate. - **Email and verification records** — Once the zone is here, the TXT/MX records for your mail provider and any domain-verification tokens are managed on this page too. | Surface | Availability | | --- | --- | | Portal UI | The setup checklist with live progress, zone/NS/certificate status, and DNS record management | | CLI | `snoozestack domains claim/status/list/delete/primary/retry` and `domains records …` | | SDK / HTTP | None — script it via the Management API's domains/records endpoints instead | ### Connecting a domain ```bash title="snoozestack/project.toml + terminal" [[domains]] name = "example.com" ``` ```bash title="terminal" snoozestack domains claim example.com # idempotent — re-running reports status snoozestack domains status # the checklist, incl. the NS servers to set ``` One domain per project, since the certificate covers the apex and its subdomains. Claiming is deliberately its own step rather than part of `snoozestack publish` — the infrastructure work is asynchronous and can take minutes to hours, so publish only validates the declaration and reports drift, never claims or disconnects. snoozestack finds or creates the public hosted zone and requests a certificate for `example.com` + `*.example.com`, inserting the DNS validation records into the zone automatically. That takes a few seconds, and then the page shows the zone's **name servers**. A name already connected somewhere else on snoozestack is refused, and so is one whose zone here belongs to another organization — a domain can only be claimed once, in either direction between an apex and its subdomains. Projects in the same organization are the exception: they share the zone, and it is torn down when the last of them disconnects. The certificate then sits at `pending validation` until you do your part: **set those name servers as the domain's NS entries at your registrar**. Validation runs through this zone, so the certificate can't issue while your registrar still points somewhere else — this is the one step nothing on the snoozestack side can do for you. Once the delegation is live the certificate issues by itself, usually within minutes, and the page updates on its own. ### What the domain serves The apex is set up for you as soon as the certificate issues, and it serves everything the project host does — the same paths, in the same shapes, from one edge distribution: - **`example.com/` — your primary website**. Whichever site is marked primary on the [Websites](https://snoozestack.com/docs/websites.md) page is served at the apex; make a different one primary and the apex follows it, without the domain going down in between. The edge reads the site's published files directly (the portal is not in the serving path) with the same semantics as the project URL: `index.html` for directory URLs, trailing-slash redirects, assets cached at the edge. - **`example.com/sites//` — your other websites**, at the same paths they already have on the project host. - **`example.com/api/…` — the project API**, the same `/api/auth`, `/api/functions/v1`, `/api/storage/v1` and `/api/webhooks/…` the project host answers. A published browser app calling `/api` on its own origin therefore works on both, with nothing to rebuild. Nothing moves off `.snzzz.com` to make that happen. Both URLs stay live for as long as the domain is connected, which is what makes the choice below instant and reversible. Three things stay on the project host permanently and by design: a server-side function's own `SNOOZESTACK_URL`, the OAuth provider callback you registered at Google or Apple, and the hosted `/auth/reset` and `/auth/confirm` pages — none should depend on a DNS change you might make later, and a verify link already sitting in an inbox has to keep arriving somewhere. The **preferred origin** (`snoozestack domains primary platform|custom`, or the switch next to the domain name in the console) chooses which of the two URLs snoozestack *hands out*: copy buttons, site and function URLs, webhook URLs, SDK snippets. It stays on for the whole of a connect — asking for a domain is asking to be served at it — and the cutover happens as the last step of the setup, once every route has been requested on the domain and answered. Until then the project URL keeps being handed out, and while setup is running the switch is locked: **Disconnect** is how you change your mind, not the switch. There is no DNS to wait on, so it takes effect at once and can be flipped back just as fast. It never takes either URL down; links you have already shared keep working whichever way it is set. Only **Disconnect** gives the domain up. Auth emails and redirects follow the preferred origin too, and both of the project's own origins stay on the [redirect allowlist](https://snoozestack.com/docs/authentication.md) either way, so links already sitting in an inbox keep working. Turning it *on* also re-runs the setup checks, and the checklist goes live again while they do: the edge routing is compared against what the project should have and rebuilt if it has drifted, DNS is re-checked, and every route is requested to confirm it answers. That is the point at which your project starts depending on the domain, so it is the right moment to prove the domain still works rather than trust a connection that finished weeks ago. A rebuild takes the few minutes the edge needs to deploy; the domain keeps serving throughout, and if a check fails the step says which route and offers a retry. The distribution follows a pay-as-you-go setup — SNI certificates, no WAF, no Origin Shield, nothing with a fixed monthly cost. Disconnecting tears it down: the alias comes off and the distribution is disabled immediately, then deleted once the edge finishes deploying the disabled state, which is the few minutes the last step of the disconnect waits on. ### DNS records The zone's records are listed inline, and you can add or delete `A`/`AAAA`/`CNAME`/`TXT`/`MX`/`SRV` records — e.g. the TXT and MX entries for your mail provider. The zone's SOA/NS records and the edge alias records are managed for you. Disconnecting a domain moves generated links back to the project URL and checks it is healthy before removing anything, then releases the edge alias, deletes the records snoozestack owns, tears the distribution down and deletes the hosted zone with all of its records — so keep a copy of any records you added yourself. It runs to completion whether or not you stay on the page. The certificate stays in your account, and reconnecting later creates a fresh zone with new nameservers to set at your registrar. If you only want snoozestack to stop handing the domain out, turn the switch off instead — that leaves everything serving. --- # Push Notifications > Send iOS push notifications from a queue: register devices from the Swift SDK, enable APNs on your existing Apple key, and put a message naming a user on a queue. [HTML version](https://snoozestack.com/docs/push-notifications/) · [Docs index](https://snoozestack.com/llms.txt) Push is built so the flow is yours and the delivery machinery isn't. Declare a notification service in `project.toml` — it signs with the Apple key you already use for sign-in — and send two ways: directly from a [function](https://snoozestack.com/docs/functions.md) with the `push` capability, or by putting a message naming a user on a [queue](https://snoozestack.com/docs/queues.md) whose trigger has the service as a destination. Your app registers its device token once; finding a person's devices, signing to Apple, retrying, and retiring dead tokens are all automatic. **Why use it** The hard parts of push are not the sending. They are knowing which devices belong to a person, keeping that list clean as people reinstall and switch phones, holding an HTTP/2 connection to Apple with a signed token that expires every hour, and retrying without notifying somebody twice. Modelling it as a queue destination gets all of that from machinery that already exists: the queue holds the message until it is delivered, the trigger reports what happened, and a device that Apple reports as gone is retired automatically. What you write is the one line that puts a message on the queue. - **Telling a user something finished** — A booking confirmed, a render completed, an order shipped. The function that already reacts to the change enqueues one message; the user's phone lights up whether the app is open or not. - **Reacting to a database change** — A row lands, a function sees it, and the person who cares hears about it — without your backend holding a connection to Apple or knowing which devices that person has. - **Silent background refresh** — A message with `data` and no `notification` becomes a background push: nothing is shown, but the app is woken to fetch what changed. | Surface | Availability | | --- | --- | | Portal UI | Queues → Notifications: declared services, registered devices, and a test send. Apple credentials live with the provider declaration — see [Authentication](https://snoozestack.com/docs/authentication.md) | | CLI | `snoozestack notifications list/push/enable/test/devices/register-device` | | SDK / HTTP | `snoozestack.push.register(deviceToken:)` (Swift). Sending is server-side: `capabilities.push.send()` in a function, or a queue message | ### Locally Under `snoozestack dev` the device registry is real and local: an app pointed at the dev server registers its token at `/push/v1/devices` (with the local anon key) and the Notifications page lists it. A function's `capabilities.push.send()` resolves the target against that registry and records the send — devices matched, payload as sent — to `.snoozestack/outbox/push/` and the same page, without contacting Apple; the result carries `recorded` instead of a delivery. When a real device must buzz, start with `snoozestack dev --push=sandbox` (or `[dev] push = "sandbox"` in `snoozestack/dev.toml`) and sends take the hosted sandbox gateway, which needs the directory linked and logged in. ### What to do at Apple, once Two things in the developer portal and one in Xcode. If you already use Sign in with Apple, all three edit what you already have — no new key, no new App ID. 1. **Identifiers → your App ID → tick Push Notifications → Save.** It must be an explicit App ID (`com.example.myapp`); wildcard IDs cannot have push. Enabling the capability invalidates existing provisioning profiles — Xcode's automatic signing regenerates them on the next build. 1. **Ignore the certificate offer.** Ticking the box makes Apple offer to create Development and Production SSL Certificates. Skip both. Those belong to the older certificate-based authentication, which snoozestack does not use; they expire every 12 months, per app, per environment, and nothing here needs them. 1. **Keys → your Apple key → Edit → tick Apple Push Notifications service (APNs) → Save.** The same key can carry both capabilities, so the one already signing your Sign in with Apple tokens is the one to use. If you don't have a key yet, create one with both ticked and download the `.p8` — it downloads *once*. 1. **In Xcode: target → Signing & Capabilities → + Capability → Push Notifications.** This writes the entitlement that makes `registerForRemoteNotifications()` return a token. Add **Background Modes → Remote notifications** too if you want silent pushes. Editing an existing key does not change the key or invalidate the `.p8` you already have. Which services a key may sign for is a record on Apple's side, keyed by the Key ID; the private key itself carries none of it. That is why Apple lets you edit a key it will never let you download again — and why the copy snoozestack already holds starts working for push within a few minutes of you ticking the box. It also keeps you inside Apple's limit of **two auth keys per team**, which is the constraint worth designing around: keys are scarce, and one key doing both jobs spends none of that budget. ### Turning it on There are no new credentials to paste — push signs with the Apple key your project already declares for Sign in with Apple (see [Authentication](https://snoozestack.com/docs/authentication.md)); both the key and its App ID just need the APNs capability ticked at Apple. Declare the service and publish: ```bash title="snoozestack/project.toml" [[push]] name = "apple" # what a queue trigger's --notify names provider = "apns" ``` Then send a test push before wiring anything else up — it reports Apple's raw status and reason, which is the difference between diagnosing push and guessing at it: ```bash title="terminal" snoozestack notifications test --service apple --device --title "Hi" --body "It works" ``` ### Sandbox and production Apple runs two entirely separate push systems, and which one a device belongs to is decided when the app is *built*: | How the app got on the phone | APNs world | | --- | --- | | You hit ⌘R in Xcode | sandbox | | TestFlight or the App Store | production | A token from an Xcode build only works against sandbox; a TestFlight token only against production. Both are 64 hex characters and there is no way to tell them apart by eye — sending to the wrong one returns `BadDeviceToken`, which looks exactly like a corrupt token. This is the single most common cause of “push doesn't work”. You do not have to manage it. One `.p8` signs for both worlds, the SDK detects which one the build belongs to and records it when the device registers, and each send is routed to the matching Apple server. A debug build on your phone and an App Store install on your iPad both work, at the same time, with no configuration. ### Registering a device Ask for permission when it makes sense in your app — the SDK deliberately does not do this for you — then hand the token to snoozestack. Call it on *every* launch: tokens change on reinstall and on restore-to-a-new-device. A repeat registration of an unchanged token costs no request. ```swift title="AppDelegate.swift" import SnoozeStack // Ask for permission wherever it belongs in your onboarding, then: UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) UIApplication.shared.registerForRemoteNotifications() // must be on the main thread func application(_ app: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data) { Task { try? await snoozestack.push.register(deviceToken: token) } } // On sign-out: try await snoozestack.push.unregister() ``` If someone is signed in, the SDK attaches their session and the server binds the device to their user id — the same id a [function](https://snoozestack.com/docs/permissions.md) sees as `capabilities.auth.user.id`. You never pass a user id yourself. A device that registers before anyone signs in is kept unattached, and the next launch binds it. Under the hood this is a `POST /push/v1/devices` on the project host, so a platform without the Swift SDK can register the same way. The device registry is managed server-side and isn't client-readable, so nothing a shipped app holds can enumerate your users' device tokens. During development, `snoozestack notifications register-device` registers a real device token against the sandbox environment so a function running under `snoozestack dev` can deliver an actual notification — with no device registered, `capabilities.push.send()` reports a dry run (“would have sent to N devices”) instead of failing. ### Sending one The direct path is one call in a function that declares the `push` capability: ```bash title="inside a function" await capabilities.push.send({ to: { user_id: user.id }, notification: { title: "Tee time confirmed", body: "Saturday, 9:40am" }, data: { booking_id: "42" }, }); ``` For durability and fan-out — or to notify from something that isn't a function — put the same JSON on a queue whose **trigger** names the notification service (`snoozestack triggers create … --notify apple`). Either way, everything else — finding the person's devices, signing the request to Apple, retrying, retiring dead tokens — is automatic. ```bash title="the message" { "to": { "user_id": "8f14e45f-ceea-467a-9575-3f0b1c2d4e5a" }, "notification": { "title": "Tee time confirmed", "body": "Saturday, 9:40am" }, "data": { "booking_id": "42" } } ``` `to` accepts `user_id`, `user_ids` (several people), `tokens` (explicit device tokens, for testing), or `distinct_id` (the [Signals](https://snoozestack.com/docs/signals.md) identity, for devices with nobody signed in). Adding `"environment": "sandbox"` inside `to` narrows a send to your own debug devices — handy for trying a payload shape without pushing to real users. | Field | Meaning | | --- | --- | | `notification` | Title, subtitle, body, badge, sound. Omit it entirely (with `data` present) for a silent background push. | | `data` | Your own key/values, delivered to the app alongside the notification. | | `collapse_id` | Replaces any undelivered notification with the same id, instead of stacking. | | `priority` | 10 (immediate, the default for alerts) or 5. Background pushes are always 5 — Apple rejects them at 10. | | `expiration` | Unix time after which Apple stops trying; 0 means one attempt only. | ### What is stored, and what is not | Thing | Where it lives | How it goes away | | --- | --- | --- | | The queued message | the queue | Deleted as soon as it is delivered; anything undeliverable dead-letters or ages out on its own. | | Device tokens | the managed device registry | Retired automatically when Apple reports the app was uninstalled. No cron job, no maintenance. | | The notification content | nowhere | It is not kept. Once sent, it is gone. | That last row is deliberate: there is no per-user notification history, no “resend”, and no in-app inbox. If you want one, write the notification to your own table when you enqueue it — you are the only one who knows what it should look like afterwards. Note that Apple gives no delivery receipt in any case: the strongest claim available anywhere is “accepted by APNs”, never “shown to the user”. ### When something fails The trigger records the outcome of its last run — the Triggers card on the console's Queues page and `snoozestack triggers list` both show it, as `2/3 delivered` plus Apple's reason for each failure — and a test send from the Notifications page reports the same raw status. The common ones: | Reason | What it means | | --- | --- | | `BadDeviceToken` | Almost always the wrong environment (see above). The device is retired; re-registering from the app brings it back. | | `Unregistered` | The app was deleted from that device. Retired automatically. | | `DeviceTokenNotForTopic` | The bundle ID on the service does not match the app the token came from. | | `InvalidProviderToken` | The team ID, key ID or `.p8` don't agree — or the key doesn't have the APNs capability ticked. | A message is only left on the queue to retry when nothing got through at all. If one device in a fan-out of hundreds hits a transient Apple error, the message is not redelivered to all of them — being notified twice is worse than the one device missing out. --- # Signals > Events, logs, metrics, traces, and audits in one store — a console log viewer, a CLI tail, and an export API. [HTML version](https://snoozestack.com/docs/signals/) · [Docs index](https://snoozestack.com/llms.txt) One envelope for everything the app reports: **events** your apps track, **logs** your functions print, plus **metrics**, **traces**, and **audits**. Apps send events with the SDK; functions emit anything with `capabilities.signals.emit(type, name, props)`; and everything lands in the project's signals store — a SQLite file the project's own runtime owns and replicates, next to its database — read in the console, streamed by the CLI, and readable through the query/export API below. The ingest key is write-only: it can send signals but never read them back, which is what makes it safe to ship in browsers and mobile apps. **Why use it** Logs tell you what your *servers* did; Signals tells you what your *users* did. Those are different questions, and the second one can't be reconstructed after the fact — if you didn't record that someone opened the checkout screen and then left, no amount of querying production tables later will recover it. Your database holds current state (this user has a subscription), not the sequence of actions that produced it. The design point here is one vocabulary for the product *and* the system. Because a function's log lines, its emitted events, and the client's tracked events share an envelope and a store, “what did this user do” and “what did the software do to them” are the same query — and no user data leaves for a third-party analytics service, which is often the deciding factor for anything privacy-sensitive. Instrument deliberately: a handful of events named after user intent (`checkout_started`, `invite_sent`) is far more useful than tracking every click. Event names are effectively permanent — every query and dashboard you build on them breaks when one is renamed, so pick names you can keep. - **What broke, and when** — A function's errors stack in red on the histogram, so a burst of failures is visible as shape before you read a line — then filter to `log` + Error and expand the row. - **Did the feature land?** — Ship the change, then compare event volume before and after — the overview aggregates on the management API, or the export API into a dashboard of your own. - **Debugging a specific user's report** — “It didn't work for me yesterday” — filter the stream to their id (the Person box on the console's Signals page, or `snoozestack signals --user `) and read the exact sequence of what they did and what the software did to them, in one timeline. - **Your own dashboard** — The export API hands every signal to any tool you already use, so a funnel or a retention chart is a query you own rather than a page you wait for. ### In the console **Signals** is one page, and it is a log viewer rather than an analytics dashboard — the instrument you open at 2am. The sidebar facets the window by signal type (`event`, `log`, `metric`, `trace`, `audit`) and, for logs, by level, with live counts; a histogram shows the shape of activity over the window with errors stacked on top; the table filters by message text or by one person's id, and any row expands in place to show every property (its `distinct_id` is a click away from becoming the filter). Hosted, the window is 24 hours, 7, 30, or 90 days; the local dashboard shows the latest signals in memory with a Refresh button in the same spot. | Surface | Availability | | --- | --- | | Portal UI | One log-viewer page: facet by type and level, filter by text, histogram over the window, rows expand in place — read-only | | CLI | `snoozestack signals` streams the hosted feed; `signals tail/query/clear` work the local dev store | | SDK / HTTP | `snoozestack.signals.*` in the app, `capabilities.signals.emit()` in a function | **Diagram:** capabilities.signals.emit() · in a function · signals.track() · in the app, batched · Signals store · event · log · metric · trace · audit · Console + CLI · stream, charts, tail · Query / export API · bring your own dashboard ### From a function A function that declares the `signals` capability writes with one method — the type is what separates a product event from a log line or a metric: ```bash title="inside a function" capabilities.signals.emit("event", "note_created", { userId: user.id }); capabilities.signals.emit("metric", "render_ms", { value: elapsed }); capabilities.signals.emit("audit", "note_deleted", { by: user.id, noteId }); ``` Under `snoozestack dev` these land in the local store (`.snoozestack/dev-signals.db`) — tail them while you build, without sending development noise to hosted ingestion: ```bash title="terminal" snoozestack signals tail --type event,log # watches live; Ctrl+C to stop snoozestack signals query --name note_created snoozestack signals clear ``` ### JavaScript / TypeScript Every `snoozestack-js` client has a `signals` handle. Events are batched in memory and flushed every 10 seconds, at 20 queued events, and when the page is hidden — `track()` never blocks and never throws. ```bash const snoozestack = createClient(SNOOZESTACK_URL, SNOOZESTACK_ANON_KEY); snoozestack.signals.track('page_view'); // anonymous device id snoozestack.signals.identify(user.id); // tie events to a user snoozestack.signals.track('signup', { plan: 'pro' }); snoozestack.signals.reset(); // on sign-out await snoozestack.signals.flush(); // force-send (tests, CLIs) // Standalone (no database/auth client needed): import { SnoozeStackSignals } from 'snoozestack-js'; const signals = new SnoozeStackSignals(SNOOZESTACK_URL, SNOOZESTACK_ANON_KEY); ``` ### iOS / Swift The `SnoozeStackSignals` Swift package (iOS, macOS, tvOS, watchOS) queues events on disk — they survive app kills — and flushes on a timer, at batch size, and when the app is backgrounded. It ships in the same [snoozestack-swift](https://snoozestack.com/docs/sdk-swift.md) package (add `SnoozeStackSignals` instead of, or alongside, `SnoozeStack`). ```swift SnoozeStackSignals.configure(url: "https://.snzzz.com", apiKey: ANON_KEY) SnoozeStackSignals.shared.identify(user.id) SnoozeStackSignals.shared.track("purchase", properties: ["sku": "pro", "price": 9.99]) ``` ### HTTP API (/api/signals/v1/events) The SDKs are thin wrappers over one endpoint, so any HTTP client can send events: ```bash title="signals — /api/signals/v1/events" curl -X POST "https://.snzzz.com/api/signals/v1/events" \ -H "apikey: $ANON_KEY" -H "Content-Type: application/json" \ -d '{"events": [ {"name": "signup", "distinct_id": "u-42", "props": {"plan": "pro"}, "ts": "2026-07-06T12:00:00Z", "insert_id": "b3f1c1e2-…"} ]}' # → {"ok": true, "ingested": 1} ``` | Field | Required | Meaning | | --- | --- | --- | | `name` | yes | Event name (≤ 200 chars), e.g. page_view, signup | | `distinct_id` | no | Who did it — a user id or device id; powers the unique-user counts | | `props` | no | JSON object of properties (≤ 8 KB serialized) | | `ts` | no | When it happened (ISO 8601 or epoch ms); defaults to arrival time. Clamped to the last 7 days / next 5 minutes | | `insert_id` | no | Idempotency key — a retried batch is never double-counted | Up to 100 events per request. `ts` is clamped into a window around now — no earlier than 7 days ago, no later than 5 minutes ahead — so events queued on a device with a wrong clock still land somewhere sane, but **historical backfill is not possible**: importing a year of events from another tool would file them all under the 7-day floor. The key can come as an `apikey` header, `Authorization: Bearer`, or an `?apikey=` query parameter (for `navigator.sendBeacon`). Raw events show up on the console's Signals page and are readable in bulk through the export API below; the aggregates (events over time, top names, people) are available to scripts via the [management API](https://snoozestack.com/docs/management-api.md). ### Retention and ingestion quota Signals are kept for a per-project retention window — **90 days by default** — after which the project's runtime deletes them; ask us to raise it if you need longer history. Ingestion is also subject to a per-project rolling 24-hour volume quota (**1,000,000 signals/day by default**). A batch that would push the project over quota is rejected outright with `429`, never partially ingested or silently dropped: ```bash title="429 — over quota" curl -i -X POST "https://.snzzz.com/api/signals/v1/events" \ -H "apikey: $ANON_KEY" -H "Content-Type: application/json" \ -d '{"events": [...]}' # HTTP/1.1 429 Too Many Requests # {"error": "Signals ingestion quota exceeded for this project (resets on a rolling 24h window). Contact support for a higher limit."} ``` Both numbers are per-project account limits, same as your project/table/storage limits — contact us to raise either one. ### Export API (/signals/v1/query) The ingest endpoint above is write-only — a key can send signals but never read them back through it. For building an external dashboard, polling for new signals, or a one-time bulk export, use the export endpoint instead: same public key-based auth as ingest, but`GET` and read-only. ```bash title="query — /api/signals/v1/query" curl "https://.snzzz.com/api/signals/v1/query?type=event&since=2026-08-01T00:00:00Z" \ -H "apikey: $ANON_KEY" # → application/x-ndjson, newest first, one JSON object per line: # {"id":"9214","type":"event","name":"signup","distinct_id":"u-42","ts":"2026-08-22T18:04:11.000Z","props":{"plan":"pro"}} # {"id":"9213","type":"event","name":"page_view","distinct_id":"u-42","ts":"2026-08-22T18:03:58.000Z","props":{}} ``` | Param | Default | Meaning | | --- | --- | --- | | `type` | event | One of event / log / metric / trace / audit | | `since` | now − 24h | ISO 8601 or epoch ms — start of the window (inclusive) | | `until` | now | ISO 8601 or epoch ms — end of the window (exclusive) | | `limit` | 1000 | Rows per page, max 5000 | | `cursor` | — | Continuation token from a prior response's X-Next-Cursor header | Each line has the same field names the ingest endpoint accepts (`id`, `type`, `name`, `distinct_id`, `ts`, `props`), so a consumer never has to reverse-engineer an internal shape. If a page doesn't reach the end of the requested window, the response carries an `X-Next-Cursor` header — pass it back as `?cursor=` to fetch the next page; a response with no such header means you've reached `since`. The key can come as an `apikey` header, `Authorization: Bearer`, or an `?apikey=` query parameter, same as ingest. ### Identity — who a person is Until you call `identify()`, events carry an anonymous id (`$anon-…`) that the SDK mints once and stores — in `localStorage` on the web, `UserDefaults` on iOS — so a returning visitor is the same person, not a new one. There is no fingerprinting: nothing is derived from the device, IP, or browser characteristics. When `identify()` runs, the SDK reports both ids and snoozestack records that they are the same person. Everything the visitor did before signing up stays attached to their account, so an export that spans sign-up sees one person and unique-user counts stop counting one human twice. The id is persisted too, so a page reload keeps them identified — call `identify()` on every load, it's a no-op when nothing changed. ```bash snoozestack.signals.track('landing_view'); // $anon-3f2a… (anonymous) snoozestack.signals.identify('u-42'); // → both ids are now one person snoozestack.signals.track('signup'); // u-42 snoozestack.signals.alias('legacy-id-9'); // link an id you minted elsewhere snoozestack.signals.reset(); // sign-out: start a fresh person ``` | Rule | Why | | --- | --- | | Two account ids never merge | A shared laptop or kiosk would otherwise chain everyone who signed in on it into one person. Applies transitively, so linking through a shared device is refused too | | A person stops merging past 20 ids | Anything larger is a shared device, not a person. The merge is refused rather than allowed to swallow the project | | `reset()` ends device continuity | Sign-out means the next person on this browser starts clean instead of inheriting the last one's identity | | Reserved `$`-prefixed events | `$identify`, `$alias`, and `$reset` are identity plumbing. They are stored (visible in the stream) but never counted as activity in the people listing or the overview aggregates | Resolution applies at query time, so the people listing (`snoozestack signals --user`, or `/signals/users` on the management API) lists people rather than ids — labelled with the account id, with how many ids merged into it. Raw signal rows are never rewritten; the identity graph lives alongside them. Identity is recorded from the moment you deploy this — links that were never captured can't be reconstructed, so history from before stays as it was. ### Anonymous identity that survives storage clearing Browsers evict script-writable storage aggressively — Safari caps it at seven days — so an anonymous visitor can come back looking brand new. Where the project API shares your site's domain, snoozestack also sets a first-party `sb_did` cookie and uses it to reconnect a cleared visitor to who they were. It is `HttpOnly`, holds nothing but an opaque id, and no code of yours has to touch it. | Setup | Anonymous identity survives | | --- | --- | | Site hosted on snoozestack (`.snzzz.com`) | Fully — the API is same-origin, so the cookie is first-party | | App and API on the same origin via the relative `/api` base | Fully — the API is same-origin however the site is reached | | Default `.snzzz.com` API, app on another domain | Until storage is cleared — the cookie is third-party there and browsers drop it, so identity falls back to localStorage alone | There is nothing to configure for any of these. The one that costs identity is an app calling the project across origins; serving the app from SnoozeStack | on the project URL or on a [custom domain](https://snoozestack.com/docs/domains.md) — keeps the call same-origin and the cookie first-party. --- # Logs > Runtime output is a signal: tail it locally, stream it hosted. [HTML version](https://snoozestack.com/docs/logs/) · [Docs index](https://snoozestack.com/llms.txt) Logs are [Signals](https://snoozestack.com/docs/signals.md) with `type = log`: a function's `console.log`/`console.error` output and the runtime's own request records join your product events, metrics, traces, and audits in the same envelope and the same store — so “what exactly happened at 2:14pm” and “what was the user doing at 2:14pm” are one timeline, not two systems. **Why use it** Logs are for the question no dashboard answers: *what exactly happened to this one request?* Metrics tell you error rates went up; only the log line tells you which constraint was violated, and on whose row. Because the whole backend is one [runtime](https://snoozestack.com/docs/runtime.md), a user action doesn't scatter across services you have to read separately — the sign-in verification, the function it reached, and that function's query all happened in one process, and their records land in one ordered stream. Which is also the argument for logging deliberately in your functions: a `console.log` with the ids involved is often the only record of why a background job made the decision it did — nobody was watching when it ran. | Surface | Availability | | --- | --- | | Portal UI | The Signals stream, filtered to logs — same console page as everything else the app reports | | CLI | `snoozestack logs ` hosted; `snoozestack signals tail --type log` locally | | SDK / HTTP | None to write logs — `console.log` in a function is captured; `capabilities.signals.emit("log", …)` for structured ones | ### Locally ```bash title="terminal" snoozestack dev --verbose # request lines in the terminal snoozestack signals tail --type log # the local store, live snoozestack signals query --type log --lines 200 ``` ### Hosted ```bash title="terminal" snoozestack logs functions --lines 100 # the runtime's own log (auth | storage | functions all read it) snoozestack logs queues # queue and website activity: queues | sites snoozestack logs auth --project my-app ``` Logs are project-scoped, and bulk reads go through the same [export API](https://snoozestack.com/docs/signals.md) (`?type=log`) as every other signal type — so an external log viewer is a consumer of one documented endpoint, not a special integration. For “who changed the project” rather than “what did it do,” see `snoozestack audit` — a change history across the console, CLI, and MCP. --- # MCP > Connect an AI client to your project's management MCP endpoint: tokens, the tool catalogue, and the docs it serves alongside them. [HTML version](https://snoozestack.com/docs/mcp/) · [Docs index](https://snoozestack.com/llms.txt) Every project exposes a **management MCP endpoint** at `https://snoozestack.com/api/mcp/`. Point an AI client at it — Codex, Claude Code, Claude Desktop, Cursor, anything that speaks the **Model Context Protocol** — and it can operate the project the way you do in the portal: read the database, deploy functions, set secrets, create buckets and queues, publish websites, configure auth, read logs and analytics. Don't confuse it with the [MCP endpoints you write](https://snoozestack.com/docs/functions.md) — functions declared with `mcp = true` that expose *your app's* tools to AI clients, served on the project host at `/api/mcp/v1/`. Those are your product's data plane. This page is the control plane: the endpoint that configures the project itself, and it ships with snoozestack rather than being something you build. **Why use it** An agent writing your app already needs a backend behind it — a table and the policies that guard it, a bucket, a function holding an API key. Without a control plane it can only write code and then hand you a list of things to go and click, which is where the loop breaks: it can't see whether the migration applied, so it guesses, and you become the integration layer between an agent and a dashboard. The useful part isn't automating the clicking. It's that the agent can **observe what it did** — read back the schema it changed, tail the logs of the function it deployed, query the rows it inserted. That closes the write-then-verify loop that separates an assistant that suggests changes from one that can actually finish a task and know it worked. The trade-off is that a token here is real access. `secrets_set` changes what your functions are handed, and `queue_delete` means it. The token is scoped to one project and revocable in a click, but treat it as production credentials: give an agent its own project to work in when the work is exploratory, and keep the token for a project with real user data as deliberate a decision as handing someone the portal password. - **Building the backend alongside the app** — The agent writing your checkout page defines the `orders` table, writes the [function](https://snoozestack.com/docs/functions.md) that reads and writes it through `capabilities.db`, publishes, and puts the Stripe key in Secrets — in the same pass as the client code, instead of leaving you a SQL snippet to paste. - **Debugging what it just shipped** — Deploy a function, call it, read its logs, fix the bug, redeploy. Every step is a tool, so the agent iterates on real output rather than asking you what the error said. - **Answering questions about the running system** — “Which buckets are public?” “What redirect URLs does auth accept?” “How many signups this week, and from where?” — schema, configuration and [Signals](https://snoozestack.com/docs/signals.md) are all readable, so the answer comes from the project instead of from memory. - **Routine operations you'd rather describe than perform** — Rotating a secret, taking a backup before a risky migration, republishing a site and flushing its CDN. ### Connecting a client Open the project's **Overview** and create an **MCP access token**. The value — it starts with `sb_mcp_` — is shown *once*, along with a ready-made setup prompt; only its hash is stored, so a lost token is rotated, never recovered. ```bash title="~/.codex/config.toml" [mcp_servers."snoozestack-"] url = "https://snoozestack.com/api/mcp/" http_headers = { Authorization = "Bearer sb_mcp_..." } ``` Restart Codex after adding the server, then use `/mcp` to verify the connection. The Codex CLI, IDE extension, and desktop app share this configuration. ```bash title="terminal" claude mcp add --transport http snoozestack- \ https://snoozestack.com/api/mcp/ \ --header "Authorization: Bearer sb_mcp_..." ``` Any other MCP client wants the same two facts — the URL and the header. The transport is **Streamable HTTP**, stateless: the client POSTs a JSON-RPC message and gets one JSON response back, with no session to keep alive. ```bash title="mcp client config" { "mcpServers": { "snoozestack-": { "type": "http", "url": "https://snoozestack.com/api/mcp/", "headers": { "Authorization": "Bearer sb_mcp_..." } } } } ``` Note the host: the management endpoint lives on `snoozestack.com`, not on your project host. `.snzzz.com/api/mcp/v1/…` is where *your own* MCP functions serve. ### Tokens A token belongs to one project and one client — name them after the client (“Codex on my laptop”) so the list stays meaningful. The table on the project **Overview** shows each token's prefix, when it was created, and **when it was last used**, which is how you tell a live integration from one you set up months ago and forgot. | Action | What happens | | --- | --- | | **Rotate** | Issues a new secret for the *same* token — the name and its place in the list stay put, so the client only needs its header updated. The old value stops working immediately, which makes this the fix for a leaked or lost token. | | **Revoke** | Deletes the token. The next call from anything holding it gets a `401`. Use it when a client, a laptop, or a person goes away. | A token acts as the account that created it and inherits that account's standing — suspend the account and its tokens are held back exactly as the portal holds back the person. If that account is later removed from the organization, the owner takes over, so someone leaving doesn't silently break every agent wired to the project. Tokens are created, rotated and revoked in the portal only: a client holding one can't mint itself another. ### What it can do Every tool maps to the same operation the portal screen performs, so anything you can do on a project's pages an agent can do through the endpoint — schedules, secrets, queues, storage, websites, domains, auth, and the logs, audit trail and analytics that say what happened. The full list, with what each tool is for and whether it reads or changes the project, is the [MCP tool catalogue](https://snoozestack.com/docs/mcp-tools.md); it is generated from the endpoint's own tool table, so it is what `tools/list` returns. Two things are deliberately *not* tools. Rows are read and written by your functions through `capabilities.db`, and the schema is `schema.mjs` in the repo — an agent can see the tables the runtime built (`tables_list`) but not query them. And functions are published from the repo as one tree by `snoozestack publish`; an agent changes one by editing the checkout, the same as a person. Tools are annotated as read-only or destructive, so a client can show you a confirmation prompt for `queue_delete` and not for `logs_read`. Failures come back as the portal's own error message — a limit hit, a bad cron expression, a bucket name that isn't allowed — so the agent can correct itself rather than reporting a dead end. ### The docs come with it Knowing the arguments to `schedule_create` isn't the same as knowing how schedules are meant to be used, so the endpoint serves **these docs** too. Every page you're reading here is available through the same connection, in markdown: | Tool | What it gives the model | | --- | --- | | `docs_index` | The table of contents, grouped as this sidebar is — every page with its summary, its sections, and **the tools that page explains**. | | `docs_read` | A page, or a single section of a long one, so a client spends context on the part that answers its question. | | `docs_search` | The pages and sections that discuss a term, with a snippet of each — for when you know the concept but not the page. | | `sdk_versions` | The currently published CLI and SDK versions with the exact install command for each, read live from the release pointers. | Because packages are served from [version-pinned URLs](https://snoozestack.com/docs/installation.md) rather than a registry, the install lines inside the pages are resolved the same way — an agent reading the quick start over MCP is told to install the version that's current today, not the one that was current when these docs were published. Pages are also exposed as MCP **resources** at `snoozestack://docs/`, so clients that surface resources let you @-mention a page directly in a conversation. The link runs both ways, which is the point: each tool's description names the page that explains it, and each page lists the tools that operate it. An agent about to call `schedule_create` can read the cron semantics first and discover how a missed run is handled — the kind of design detail that decides whether a schedule behaves properly or silently drops a run. ```bash title="terminal" docs_search("committed function") # → permissions, functions, sdk-js docs_read("permissions") # the page, plus the tools it covers docs_read("functions", "secrets") # just that section ``` ### What a token can't do The endpoint is deliberately narrower than a portal login. A token authorizes **one project** and nothing around it: - **No other project.** The project comes from the token, not from a tool argument, so there is no parameter to point at a sibling — pointing a token at another project's endpoint URL is refused outright. - **No org-level changes.** Renaming the organization, inviting or removing members, and deleting the project all stay in the portal, even when the token's account is the owner. - **No self-issued credentials.** A client can't create, rotate or revoke MCP tokens — including its own. ### MCP, the CLI, and the Management API These are three doors into the same control plane, and they stay in step because they run the same code underneath — a capability added to the portal shows up in all of them. Choose by who's driving: | Surface | For | Credential | | --- | --- | --- | | [CLI](https://snoozestack.com/docs/cli.md) | You, in a terminal — and CI | Login token from `snoozestack user login` | | [Management API](https://snoozestack.com/docs/management-api.md) | Scripts and your own tooling | The same token, as a bearer | | MCP endpoint | AI clients | A project MCP token (`sb_mcp_…`) | | Surface | Availability | | --- | --- | | Portal UI | Project Overview: create, rotate and revoke tokens, and see when each was last used | | CLI | None — tokens are portal-only; for terminal work the CLI already covers the same operations | | SDK / HTTP | The endpoint itself: JSON-RPC 2.0 over Streamable HTTP (POST), bearer-authenticated | --- # MCP tool catalogue > Every tool an AI agent gets through a project's management MCP endpoint: what each one is for, whether it reads or changes the project, and its inputs. [HTML version](https://snoozestack.com/docs/mcp-tools/) · [Docs index](https://snoozestack.com/llms.txt) This is the complete list of tools an AI client is offered when it connects to a project's [management MCP endpoint](https://snoozestack.com/docs/mcp.md) — the answer to “what can the agent actually see and do?” before you hand it a token. Each entry says what the tool is for in plain terms and whether it only observes the project or changes it; the parameters and an example call are a click further down. It is a list of **what the endpoint exposes to an agent**, not of everything snoozestack can do. Operations kept off it on purpose — deleting the project, inviting members, minting tokens — are covered under [what a token can't do](https://snoozestack.com/docs/mcp.md#scope). If it isn't on this page, an agent holding a token has no way to do it. **How to read it** Every tool carries one of three access types, the same annotation the endpoint sends to clients so they can confirm a deletion and not a read: - **read** — only looks. Logs, the schema, the user list, analytics. Calling it changes nothing. - **write** — creates or updates something: a schedule, a secret value, a bucket's visibility, an auth setting. Reversible by another change. - **destroy** — removes something, and the endpoint offers no way to get it back. 15 of the tools are this kind; they are the ones to weigh before giving an agent a token for a project with real data. The counts on the filter chips are the shape of the whole surface: of 80 tools, 40 only read. A token is scoped to one project, so every tool here acts on the project the token was made for and no other. ## Project The project as a whole: its connection details, the publish history, and the logs and audit trail that say what has happened to it. `project_info` read Connection details for this project: its API URL, publishable and service keys, primary website and auth settings. Start here when wiring an app to the project. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` project_info() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [organizations-projects-sources](https://snoozestack.com/docs/organizations-projects-sources.md) page — the same page an agent is pointed at in the tool's own description. `logs_read` read Recent log output: "runtime" is the project's container (functions, auth, storage — one process, one log); "queues" and "sites" are the queue-trigger and website activity the portal logs for it. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `service` required | string | Which log to tail One of: `runtime`, `queues`, `sites` | | `lines` | integer | How many lines (default 200, max 1000) | **Example** ``` logs_read({ "service": "runtime" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [logs](https://snoozestack.com/docs/logs.md) page — the same page an agent is pointed at in the tool's own description. `audit_list` read Recent changes to this project and who made them — from the portal, the CLI, or an AI agent (credited to its MCP token's name). Newest first. Use it to see what has already been changed before changing more, or to explain how the project got into its current state. Reads are not recorded, so this call does not appear in its own results. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `area` | string | Narrow to one part of the project One of: `auth`, `storage`, `functions`, `sites`, `domains`, `database`, `queues`, `signals`, `project` | | `actor` | string | Narrow to where the change came from One of: `portal`, `cli`, `mcp` | **Example** ``` audit_list() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [portal-tour](https://snoozestack.com/docs/portal-tour.md) page — the same page an agent is pointed at in the tool's own description. `releases_list` read The project's releases, newest first: every immutable package a `snoozestack publish` cut and deployed to production — its number, who published it, what it contains, what it dropped from the database (with how many values each dropped column or table held), and whether it is live. This reads history; it does not trigger a publish. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` releases_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [releases](https://snoozestack.com/docs/releases.md) page — the same page an agent is pointed at in the tool's own description. `release_get` read One release in full: its manifest (artifact ids, a sha256 per file, the schema plan against the release production served when it was published, what it dropped, the declarations it applies, provenance), what it changed against that release, and every deployment of it. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `release` required | string | Release number ("12") or id, from releases_list | **Example** ``` release_get({ "release": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [releases](https://snoozestack.com/docs/releases.md) page — the same page an agent is pointed at in the tool's own description. `release_deploy` write Deploy a recorded release to production again — `snoozestack publish --release `. The exact package, by artifact id, never a rebuild: the runtime and site pointers move, then the package's queues, triggers, webhooks, notification services, schedules and auth declarations are applied in the manifest's order. An older release than the live one is a rollback, recorded as one. A rollback restores code, not data: the schema stays where it is, and nothing a later release added is dropped. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `release` required | string | Release number or id from releases_list | **Example** ``` release_deploy({ "release": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [releases](https://snoozestack.com/docs/releases.md) page — the same page an agent is pointed at in the tool's own description. `deployments_list` read Every deployment of this project, newest first: each publish and rollback — which release went live, from which, who did it, and how production answered afterwards. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` deployments_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [releases](https://snoozestack.com/docs/releases.md) page — the same page an agent is pointed at in the tool's own description. `previews_list` read The project's previews: one per person who has run `snoozestack preview`, named after them (preview-@) — its URL, its state (building / ready / failed / expired / cleaned) and how long until it expires. A preview expires thirty minutes after it last came up. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` previews_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [releases](https://snoozestack.com/docs/releases.md) page — the same page an agent is pointed at in the tool's own description. `preview_status` read One preview's status: whose it is, URL, health, seed outcome and anonymization policy, the schema plan against production, and expiry. Reading it also probes a preview that is still building. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Preview name, from previews_list | **Example** ``` preview_status({ "name": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [releases](https://snoozestack.com/docs/releases.md) page — the same page an agent is pointed at in the tool's own description. `preview_delete` destroy Clean a preview up now instead of at expiry: its container, data, queues, schedule rows and secret overrides are removed. Releases are never touched. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Preview name | **Example** ``` preview_delete({ "name": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [releases](https://snoozestack.com/docs/releases.md) page — the same page an agent is pointed at in the tool's own description. `preview_outbox` read The email a preview's runtime tried to send — captured, never delivered — so a sign-up link or confirmation can be followed without a real inbox. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Preview name | **Example** ``` preview_outbox({ "name": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [releases](https://snoozestack.com/docs/releases.md) page — the same page an agent is pointed at in the tool's own description. ## Database The tables the runtime built from schema.mjs — their shape, not their contents. No tool here reads or changes a row: an app reaches rows only through a function, and a developer reads them on the control plane with "snoozestack data" under their own login or a project service token. `tables_list` read Every table the project's runtime currently holds, with its columns and row count — the shape schema.mjs produced, read back from the running database. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` tables_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [database](https://snoozestack.com/docs/database.md) page — the same page an agent is pointed at in the tool's own description. ## Functions, schedules & secrets What the runtime is serving, the cron timers that invoke it, and the secret values functions read. Function code itself is published from the repo, not through a tool. `functions_list` read The functions the project's runtime is serving right now — each with its path, auth mode (public / optional / required), declared capabilities and the secrets it may read. This is what the last `snoozestack publish` put live; the source lives in the project's repo under snoozestack/functions//. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` functions_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [functions](https://snoozestack.com/docs/functions.md) page — the same page an agent is pointed at in the tool's own description. `schedules_list` read Cron schedules that invoke this project's functions, in one namespace. Only the default namespace's schedules ever auto-fire on the shared cron sweep — a preview's schedules only ever run via schedule_run. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` schedules_list() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [functions](https://snoozestack.com/docs/functions.md) page — the same page an agent is pointed at in the tool's own description. `schedule_create` write Invoke a function on a UTC cron timer (five fields: minute hour day month weekday). Only a "default"-namespace schedule ever auto-fires — one created in a preview namespace sits idle until schedule_run. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Schedule name | | `function_name` required | string | Function to invoke | | `cron` required | string | Cron expression, e.g. `"0 * * * *"` | | `payload` | object | JSON body POSTed to the function on each run | | `enabled` | boolean | Start enabled (default true) | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` schedule_create({ "name": "", "function_name": "", "cron": "0 * * * *" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [functions](https://snoozestack.com/docs/functions.md) page — the same page an agent is pointed at in the tool's own description. `schedule_update` write Change a schedule's function, cron, payload, or enabled state. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Schedule name | | `function_name` | string | Function to invoke | | `cron` | string | Cron expression | | `payload` | object | JSON body POSTed to the function | | `enabled` | boolean | Enable or disable it | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` schedule_update({ "name": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [functions](https://snoozestack.com/docs/functions.md) page — the same page an agent is pointed at in the tool's own description. `schedule_run` write Fire a schedule immediately, off-cycle. Returns the run's result. The one way to run a non-default-namespace schedule at all, since only default auto-fires. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Schedule name | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` schedule_run({ "name": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [functions](https://snoozestack.com/docs/functions.md) page — the same page an agent is pointed at in the tool's own description. `schedule_delete` destroy Delete a schedule — its function stops firing on that timer. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Schedule name | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` schedule_delete({ "name": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [functions](https://snoozestack.com/docs/functions.md) page — the same page an agent is pointed at in the tool's own description. `secrets_list` read Names of the project's secrets and which namespaces override each — the values a function reads through its `secrets` capability. Values are never returned. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` secrets_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [functions](https://snoozestack.com/docs/functions.md) page — the same page an agent is pointed at in the tool's own description. `secrets_set` write Set one or more secret values. Running runtimes pick a new value up at their next start; the declaration (which functions may read it) is project.toml's [[secrets]], not this call. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `secrets` required | object | Name → value, e.g. `{"STRIPE_KEY": "sk_live_…"}` | **Example** ``` secrets_set({ "secrets": {"STRIPE_KEY": "sk_live_…"} }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [functions](https://snoozestack.com/docs/functions.md) page — the same page an agent is pointed at in the tool's own description. `secrets_unset` destroy Remove a secret's value. The name stays declared in project.toml until the file changes. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Secret name | **Example** ``` secrets_unset({ "name": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [functions](https://snoozestack.com/docs/functions.md) page — the same page an agent is pointed at in the tool's own description. ## Queues, triggers & notifications Message queues, what runs when a message lands, the webhook URLs that enqueue one, and the push notification services a trigger can deliver through. `queues_list` read Message queues with approximate message counts, in one namespace. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` queues_list() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `queue_create` write Create a message queue. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Queue name | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` queue_create({ "name": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `queue_send` write Enqueue a message — useful for smoke-testing a queue and its triggers. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Queue name | | `body` required | string | Message body | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` queue_send({ "name": "", "body": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `queue_delete` destroy Delete a queue and every message in it. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Queue name | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` queue_delete({ "name": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `triggers_list` read Queue triggers — what runs for messages on which queue (functions, a notification service, or both), and how the last run went. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` triggers_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `trigger_create` write Process every message drained from a queue: invoke functions, send it as a push notification, or both. At least one destination is required. A message is removed only once every destination succeeded. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Trigger name | | `queue_name` required | string | Queue to drain | | `function_names` | string[] | Functions to invoke, in order | | `notification_service` | string | Notification service to push through | **Example** ``` trigger_create({ "name": "", "queue_name": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `trigger_update` write Change a trigger's destinations or enabled state. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Trigger name | | `function_names` | string[] | Replacement function list | | `notification_service` | string | Notification service to push through | | `enabled` | boolean | Enable or disable it | **Example** ``` trigger_update({ "name": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `trigger_delete` destroy Delete a queue trigger. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Trigger name | **Example** ``` trigger_delete({ "name": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `webhooks_list` read Public inbound URLs that write to a queue. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` webhooks_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `webhook_create` write Create a public URL that writes every POST straight to a queue. Add a trigger on the same queue to process the messages. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Webhook name | | `queue_name` required | string | Queue it writes to | **Example** ``` webhook_create({ "name": "", "queue_name": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `webhook_delete` destroy Delete a webhook — its public URL stops accepting messages. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Webhook name | **Example** ``` webhook_delete({ "name": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `notification_services_list` read Push destinations a trigger can send through, with the Apple team, key and bundle IDs each one signs with (never the .p8 itself). **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` notification_services_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `notification_service_enable` write Turn on push notifications. Takes no credentials: APNs signs with the project's existing Apple sign-in key. That key and the App ID must both have the APNs capability ticked in Apple's developer portal, which only the account holder can do. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` | string | Service name (default "apple") | **Example** ``` notification_service_enable() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `notification_service_delete` destroy Turn off a push destination. Triggers using it stop sending; the Apple key itself is untouched. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `name` required | string | Service name | **Example** ``` notification_service_delete({ "name": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. `push_devices` read Registered push devices: active/retired counts, a breakdown by APNs environment, and recent registrations (tokens truncated). **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` push_devices() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [queues](https://snoozestack.com/docs/queues.md) page — the same page an agent is pointed at in the tool's own description. ## Storage Buckets, the objects in them, and signed URLs for reaching private ones. `buckets_list` read Storage buckets and whether each is public, in one namespace. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` buckets_list() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [storage](https://snoozestack.com/docs/storage.md) page — the same page an agent is pointed at in the tool's own description. `objects_list` read Files in a bucket, optionally under a path prefix. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `bucket` required | string | Bucket name | | `prefix` | string | Path prefix, e.g. `"avatars/"` | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` objects_list({ "bucket": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [storage](https://snoozestack.com/docs/storage.md) page — the same page an agent is pointed at in the tool's own description. `object_upload` write Upload a file to a bucket. Pass base64 for binary content. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `bucket` required | string | Bucket name | | `path` required | string | Object key, e.g. `"avatars/me.png"` | | `content` required | string | File contents | | `encoding` | string | How `content` is encoded (default utf8) One of: `utf8`, `base64` | | `contentType` | string | MIME type, e.g. `"image/png"` | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` object_upload({ "bucket": "", "path": "avatars/me.png", "content": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [storage](https://snoozestack.com/docs/storage.md) page — the same page an agent is pointed at in the tool's own description. `object_delete` destroy Delete one file from a bucket. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `bucket` required | string | Bucket name | | `key` required | string | Object key | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` object_delete({ "bucket": "", "key": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [storage](https://snoozestack.com/docs/storage.md) page — the same page an agent is pointed at in the tool's own description. `object_sign_url` read A time-limited signed URL for one object in a private bucket. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `bucket` required | string | Bucket name | | `key` required | string | Object key | | `namespace` | string | Namespace to operate in: "default" (the live project; default when omitted) or "preview:" — a person's preview, the namespace `snoozestack preview` pushed to (names from previews_list). | **Example** ``` object_sign_url({ "bucket": "", "key": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [storage](https://snoozestack.com/docs/storage.md) page — the same page an agent is pointed at in the tool's own description. ## Websites Static sites served on the project host: uploading files, publishing a version, and choosing which one is live. `sites_list` read Published static websites with their file counts and URLs. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` sites_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [websites](https://snoozestack.com/docs/websites.md) page — the same page an agent is pointed at in the tool's own description. `site_files` read Every published file of one site with its content hash — the manifest a sync diffs against. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `site` required | string | Site name | **Example** ``` site_files({ "site": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [websites](https://snoozestack.com/docs/websites.md) page — the same page an agent is pointed at in the tool's own description. `site_upload_file` write Stage one file for a site. Upload each file, then call site_publish once to create a version — previewable immediately, never live on its own: a site goes live only as part of a release (snoozestack publish). **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `site` required | string | Site name | | `path` required | string | Site-relative path, e.g. `"index.html"` | | `content` required | string | File contents | | `encoding` | string | How `content` is encoded (default utf8) One of: `utf8`, `base64` | **Example** ``` site_upload_file({ "site": "", "path": "index.html", "content": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [websites](https://snoozestack.com/docs/websites.md) page — the same page an agent is pointed at in the tool's own description. `site_delete_files` destroy Remove specific files from a site — the prune half of a publish. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `site` required | string | Site name | | `paths` required | string[] | Files to remove | **Example** ``` site_delete_files({ "site": "", "paths": [""] }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [websites](https://snoozestack.com/docs/websites.md) page — the same page an agent is pointed at in the tool's own description. `site_delete` destroy Delete a whole site. It stops serving immediately. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `site` required | string | Site name | **Example** ``` site_delete({ "site": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [websites](https://snoozestack.com/docs/websites.md) page — the same page an agent is pointed at in the tool's own description. `site_set_primary` write Serve this site at the bare project host root. It must already be published. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `site` required | string | Site name | **Example** ``` site_set_primary({ "site": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [websites](https://snoozestack.com/docs/websites.md) page — the same page an agent is pointed at in the tool's own description. `site_publish` write Turn whatever was staged with site_upload_file/site_delete_files into a new version. Returns a preview URL — the version is viewable there immediately but does not go live: a site goes live only as part of a release (snoozestack publish, or release_deploy of a recorded one). **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `site` required | string | Site name | **Example** ``` site_publish({ "site": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [websites](https://snoozestack.com/docs/websites.md) page — the same page an agent is pointed at in the tool's own description. `site_versions` read Version history for a site — id, created_at, size, status, and which one (if any) is live. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `site` required | string | Site name | **Example** ``` site_versions({ "site": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [websites](https://snoozestack.com/docs/websites.md) page — the same page an agent is pointed at in the tool's own description. ## Domains Custom domains, their DNS records, and the CDN distributions in front of them. `domains_list` read Custom domains connected to this project, with their nameservers and certificate status. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` domains_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [domains](https://snoozestack.com/docs/domains.md) page — the same page an agent is pointed at in the tool's own description. `domain_connect` write Connect a custom domain: creates its hosted zone and certificate. Point the registrar at the returned nameservers. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `domain` required | string | Apex domain, e.g. `example.com` | **Example** ``` domain_connect({ "domain": "example.com" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [domains](https://snoozestack.com/docs/domains.md) page — the same page an agent is pointed at in the tool's own description. `domain_disconnect` destroy Disconnect a custom domain and tear down its zone. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `domain` required | string | Domain to remove | **Example** ``` domain_disconnect({ "domain": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [domains](https://snoozestack.com/docs/domains.md) page — the same page an agent is pointed at in the tool's own description. `domain_records_list` read DNS records in a connected domain's hosted zone. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `domain` required | string | Connected domain | **Example** ``` domain_records_list({ "domain": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [domains](https://snoozestack.com/docs/domains.md) page — the same page an agent is pointed at in the tool's own description. `domain_record_upsert` write Create or replace a DNS record in a connected domain's zone. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `domain` required | string | Connected domain | | `name` required | string | Record name, e.g. `mail.example.com` | | `type` required | string | Record type, e.g. `A, CNAME, TXT, MX` | | `values` required | string[] | Record values | | `ttl` | integer | TTL in seconds (default 300) | **Example** ``` domain_record_upsert({ "domain": "", "name": "mail.example.com", "type": "A", "values": [""] }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [domains](https://snoozestack.com/docs/domains.md) page — the same page an agent is pointed at in the tool's own description. `domain_record_delete` destroy Delete a DNS record from a connected domain's zone. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `domain` required | string | Connected domain | | `name` required | string | Record name | | `type` required | string | Record type | **Example** ``` domain_record_delete({ "domain": "", "name": "", "type": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [domains](https://snoozestack.com/docs/domains.md) page — the same page an agent is pointed at in the tool's own description. `distributions_list` read CDN distributions attaching custom domains to this project's websites and API. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` distributions_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [domains](https://snoozestack.com/docs/domains.md) page — the same page an agent is pointed at in the tool's own description. `distribution_create` write Attach a custom domain (or subdomain) to a published site or to the project API. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `domain` required | string | Connected apex domain | | `target_kind` required | string | What the domain should serve One of: `site` | | `site` | string | Site name when target_kind is "site" | | `subdomain` | string | Subdomain label, e.g. `"app"; omit for the apex` | **Example** ``` distribution_create({ "domain": "", "target_kind": "site" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [domains](https://snoozestack.com/docs/domains.md) page — the same page an agent is pointed at in the tool's own description. `distribution_delete` destroy Detach a distribution: the domain stops serving and its alias records go away. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `id` required | string | Distribution id from distributions_list | **Example** ``` distribution_delete({ "id": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [domains](https://snoozestack.com/docs/domains.md) page — the same page an agent is pointed at in the tool's own description. ## Authentication How users sign in: the identity service, sign-in providers, redirect URLs, email templates, and the user list itself. `identity_config_get` read The redirect-URL allowlist and OAuth provider declarations for this project's hosted application-identity service (magic-link + OAuth sign-in behind the whoami cookie) — what the last `snoozestack publish` applied from project.toml's [auth] table. Distinct from auth_config_get, which covers the separate GoTrue-backed auth service. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` identity_config_get() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_config_get` read Auth URL configuration and which sign-in providers are on. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` auth_config_get() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_config_update` write Set the site URL and the redirect URLs auth is allowed to send users back to. Restarts the auth service. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `siteUrl` | string | Where auth redirects land by default | | `additionalRedirectUrls` | string[] | Other allowed redirect URLs | **Example** ``` auth_config_update() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_provider_update` write Turn a sign-in provider on or off. For google, pass the OAuth client id; the secret can be omitted once one is stored. For apple, pass the services id plus the team id, key id and .p8, and a clientSecret from auth_apple_secret_generate. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `provider` required | string | Provider to configure One of: `email`, `google`, `apple` | | `enabled` required | boolean | On or off | | `clientId` | string | Google OAuth client id | | `clientSecret` | string | Google OAuth client secret, or generated Apple client secret | | `servicesId` | string | Apple services id — the web client the secret is signed for | | `bundleIds` | string | Bundle ids of native apps that sign in by ID token, comma-separated | | `secretExpiresAt` | string | Expiry of the Apple client secret, as returned by auth_apple_secret_generate | | `teamId` | string | Apple developer team id | | `keyId` | string | Apple signing key id | | `privateKey` | string | Contents of the Apple .p8 signing key; stored so the secret can be regenerated | | `skipNonceCheck` | boolean | Google only: accept ID tokens with any nonce (needed by some native flows) | **Example** ``` auth_provider_update({ "provider": "email", "enabled": true }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_apple_secret_generate` write Sign an Apple client secret from the project's .p8 key and return it with its expiry — Apple's secret is a token that expires after six months, not a fixed string. Saves nothing: pass the result to auth_provider_update. Services id, team id, key id and private key fall back to what's already stored. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `servicesId` required | string | Services id the secret is signed for — never a native bundle id | | `teamId` | string | Apple developer team id | | `keyId` | string | Apple signing key id | | `privateKey` | string | Contents of the Apple .p8 signing key | **Example** ``` auth_apple_secret_generate({ "servicesId": "" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_provider_delete` destroy Remove a provider's stored credentials entirely. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `provider` required | string | Provider to remove One of: `google`, `apple` | **Example** ``` auth_provider_delete({ "provider": "google" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_users_list` read End users of this project's auth service, with sign-in and confirmation timestamps. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` auth_users_list() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_user_set_banned` write Ban or unban one end user. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `id` required | string | User id | | `banned` required | boolean | Banned or not | **Example** ``` auth_user_set_banned({ "id": "", "banned": true }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_user_delete` destroy Delete an end user account. **Access** Removes something, and what it removes is not recoverable through the endpoint. Clients that honour the endpoint's annotations ask before running it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `id` required | string | User id | **Example** ``` auth_user_delete({ "id": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_user_send_email` write Send an existing user their sign-in link — which is also how a forgotten password is recovered. There is no invite or re-confirmation: accounts exist only because someone signed up. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `type` required | string | Which email to send One of: `recovery` | | `email` required | string | Recipient address | **Example** ``` auth_user_send_email({ "type": "recovery", "email": "" }) ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_emails_get` read The project's auth email templates, the from address, and whether signups require email confirmation. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` auth_emails_get() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_emails_update` write Override auth email templates and/or the email-confirmation requirement. A template set to null resets to the default. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `emailConfirmations` | boolean | Require email confirmation on signup | | `templates` | object | Type → { subject, body } or null, e.g. `{"invite": {"subject": "…", "body": "

…

"}}` | **Example** ``` auth_emails_update() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_landing_config_get` read Where the hosted password page sends users instead of rendering its own form. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` auth_landing_config_get() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. `auth_landing_config_update` write Set where the hosted password page hands off to instead of rendering its own form. Pass null for a field to clear that override. **Access** Creates or updates something in the project. Reversible by a further change. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `mobileUrl` | string | Deep link for mobile clients | | `webUrl` | string | Where browsers go instead of the hosted page | **Example** ``` auth_landing_config_update() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [authentication](https://snoozestack.com/docs/authentication.md) page — the same page an agent is pointed at in the tool's own description. ## Signals Product analytics — the events, users and metrics the SDKs report. Read-only. `signals_overview` read Product-analytics aggregates for a time range: totals, per-bucket series and the top events. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `range` | string | Time range (default 7d) One of: `24h`, `7d`, `30d`, `90d` | **Example** ``` signals_overview() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [signals](https://snoozestack.com/docs/signals.md) page — the same page an agent is pointed at in the tool's own description. `signals_events` read The newest-first stream of raw signals, filterable by type, name, user and free text. `type` defaults to "event" (product analytics); set it to log/metric/trace/audit to read those instead — they don't carry a user (distinct_id is event-only). **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `type` | string | Signal type (default event) One of: `event`, `log`, `metric`, `trace`, `audit` | | `range` | string | Time range (default 7d) One of: `24h`, `7d`, `30d`, `90d` | | `events` | string[] | Only these names | | `distinct_id` | string | Only this user's events (type=event only) | | `q` | string | Free-text match on name or user | | `before_ts` | string | Page back: `ts` of the last signal you saw | | `before_id` | string | Page back: `id` of the last signal you saw | **Example** ``` signals_events() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [signals](https://snoozestack.com/docs/signals.md) page — the same page an agent is pointed at in the tool's own description. `signals_users` read User profiles derived from events. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `q` | string | Search by user id or trait | | `offset` | integer | Row offset for paging | **Example** ``` signals_users() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [signals](https://snoozestack.com/docs/signals.md) page — the same page an agent is pointed at in the tool's own description. `signals_meta` read Event names and property keys seen in a range — the vocabulary available for filters. With `property` set, returns that key's top values instead. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `range` | string | Time range (default 7d) One of: `24h`, `7d`, `30d`, `90d` | | `property` | string | Property key to list values for | **Example** ``` signals_meta() ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [signals](https://snoozestack.com/docs/signals.md) page — the same page an agent is pointed at in the tool's own description. ## Documentation & versions These docs, served over the same connection, plus the currently published CLI and SDK versions. Read-only, and about snoozestack rather than the project. `docs_index` read The snoozestack documentation table of contents: every page with its summary, its sections, and the management tools it explains. Read this first when you need to know how a feature is meant to be used, or which tool to reach for. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` docs_index() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [mcp](https://snoozestack.com/docs/mcp.md) page — the same page an agent is pointed at in the tool's own description. `docs_read` read Read a documentation page in full, or one section of it. Covers the concepts, the design patterns snoozestack expects, and worked examples — the reasoning behind an operation rather than just its arguments. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `slug` required | string | Page slug from docs_index, e.g. `"functions" or "permissions"` | | `section` | string | Section id from the page's `sections` list; omit for the whole page | **Example** ``` docs_read({ "slug": "functions" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [mcp](https://snoozestack.com/docs/mcp.md) page — the same page an agent is pointed at in the tool's own description. `docs_search` read Search the documentation for a term and get back the pages and sections that discuss it, with a snippet of each. Use it when you know the concept but not which page covers it. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** | Parameter | Type | Description | | --- | --- | --- | | `query` required | string | What to look for, e.g. `"committed function" or "signed url"` | | `limit` | integer | Maximum hits to return (default 8) | **Example** ``` docs_search({ "query": "committed function" }) ``` Shown with the required arguments only. Anything not required is optional and defaults as its description says. **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [mcp](https://snoozestack.com/docs/mcp.md) page — the same page an agent is pointed at in the tool's own description. `sdk_versions` read The currently published versions of the snoozestack CLI and client SDKs, with the exact install command for each. Packages are served from version-pinned URLs rather than a registry, so check here before writing an install line. **Access** Only observes. Nothing about the project changes when an agent calls it. **Inputs** None. The project is never an argument — it comes from the token — so the call is bare. **Example** ``` sdk_versions() ``` **Returns** The same JSON the [Management API](https://snoozestack.com/docs/management-api.md) route behind this tool returns — the tool is a thin wrapper over it, not a separate implementation. A refused call comes back as that route's own error message. Explained on the [installation](https://snoozestack.com/docs/installation.md) page — the same page an agent is pointed at in the tool's own description. ### Where this list comes from The page is generated, not written. The endpoint's tool table — the one file that declares every tool, its description and its argument schema, and the one `tools/list` is answered from — is flattened into the data this page renders, and the portal's test suite compares the two on every run. A tool added, renamed or re-described without regenerating this page fails those tests, so what you read here is what an agent is offered, described in the same words the agent reads. The access type is the tool's own `readOnlyHint` / `destructiveHint` annotation, so it can't disagree with what a client is told. To see the live catalogue for a specific project, including any schema detail this page abbreviates, ask the endpoint directly: ```bash title="terminal" curl -s https://snoozestack.com/api/mcp/ \ -H "Authorization: Bearer sb_mcp_..." \ -H "content-type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` Everything an agent does with these tools is recorded in the project's audit log, credited to the token's name — `audit_list` above reads it, and so does the Audit page in the console. Reads are not recorded; changes and deletions are. --- # CLI reference > Every snoozestack CLI command. [HTML version](https://snoozestack.com/docs/cli/) · [Docs index](https://snoozestack.com/llms.txt) snoozestack CLI v1.56.0 · [Install or upgrade](https://snoozestack.com/docs/installation.md) The core loop is five commands — `init`, `dev`, `check`, `preview`, `publish` — over the committed `snoozestack/` folder: a preview runs your checkout in your own temporary copy of the project, and a publish ships it to production as a new release (see [Releases & previews](https://snoozestack.com/docs/releases.md)). Project-scoped commands use the `[project] name` declared in `snoozestack/project.toml` (written by `snoozestack link`), or an explicit `--project `. Most resource groups accept `--dev` / `--preview ` to scope an operation to that namespace. Add `--help` to any command for the full detail. Nearly every command below has a counterpart tool on the project's [MCP endpoint](https://snoozestack.com/docs/mcp.md), so an AI client can do this work directly instead of shelling out to the CLI on your behalf. ```bash title="the project lifecycle" snoozestack init [name] # scaffold project.toml + a starter function + schema.mjs snoozestack check # validate the manifest — file and line on failure snoozestack dev [--port ] # run the whole runtime natively; prints the dashboard link snoozestack dev reset # throw away local state, rebuild + reseed snoozestack preview # run this checkout in your own preview (preview-@) snoozestack preview status # its URL, health, seed outcome, schema plan, expiry snoozestack preview outbox # mail it tried to send — captured, never delivered snoozestack preview delete # clean it up now (it expires 30 minutes after it last came up) snoozestack publish # ship this checkout to production as a new release snoozestack publish --yes # same, answering the "this release drops data" prompt snoozestack publish --release r11 # deploy release 11 again — a rollback (code, not data) snoozestack releases # every release, newest first: live? who? what it dropped snoozestack releases show r12 # the full package: artifacts, checksums, schema plan, drops snoozestack releases deployments # every publish and rollback, who, and how production answered ``` ```bash title="account, linking & projects" snoozestack user login # opens your browser to sign in (password or Google); stores a token snoozestack user # who this machine is signed in as snoozestack user token # print that token, for scripts calling the management API snoozestack user logout # forget + revoke the stored token snoozestack link --project my-app # record the project in snoozestack/project.toml snoozestack projects list snoozestack projects create # provision a hosted project (refused at the plan's project limit) snoozestack projects rename # change the display name; the ref (hostname, database) never changes snoozestack projects stop # pause it: runtime stops, API and sites answer "paused", nothing deleted snoozestack projects start # resume it (needs a free slot under the plan's allowance) snoozestack projects keys [] # SNOOZESTACK_URL + API keys snoozestack projects tokens create # project-scoped credential for CI snoozestack projects tokens list | revoke | rotate snoozestack projects delete [--yes] ``` ```bash title="schema" snoozestack schema build # schema.mjs → snoozestack/schema.json (offline); publish ships it ``` ```bash title="functions" snoozestack functions list # the declared functions, as the project runs them snoozestack functions create [--mcp] # scaffold snoozestack/functions//index.ts + its [[functions]] entry snoozestack functions schedules list | create --function --cron "" [--payload ] snoozestack functions schedules enable | disable | delete [--yes] snoozestack functions schedules run # fire it now through the scheduler; prints the row, exits 1 unless it ran ok snoozestack mcp list # the MCP endpoints you declared, with their client URLs ``` ```bash title="secrets" snoozestack secrets set KEY=value # hosted default — running runtimes pick it up snoozestack secrets set KEY=value --dev # local-only override for snoozestack dev snoozestack secrets set KEY=value --preview ada # override for one preview (name from preview status) snoozestack secrets list # names + which namespaces override; never values snoozestack secrets unset KEY ``` ```bash title="storage" snoozestack storage list # buckets are their [[storage]] entries; publish applies them ``` ```bash title="data" snoozestack data tables # every table, with row and column counts snoozestack data rows
# one page of rows (100), tab-separated snoozestack data rows
--all # every page snoozestack data rows
--offset 100 --json snoozestack data rows
--preview ada # that preview's own database # read-only, and a management credential — never the project's anon/service key ``` ```bash title="queues, triggers & webhooks" snoozestack queues list # messages / in flight / delayed per queue snoozestack queues create [--max-receives ] snoozestack queues send # enqueue a test message snoozestack queues dead-letters # what exhausted its retries snoozestack queues delete [--yes] snoozestack triggers create --queue --function [--notify ] snoozestack triggers list | enable | disable | delete snoozestack webhooks create --queue # → https://.snzzz.com/api/webhooks// snoozestack webhooks list | delete [--yes] ``` ```bash title="push notifications" snoozestack notifications enable [] # imperative shortcut snoozestack notifications list | delete [--yes] snoozestack notifications devices # registered device tokens snoozestack notifications register-device --token # let dev send to a real device snoozestack notifications test --service --device --title … --body … ``` ```bash title="sites & domains" snoozestack sites versions # version history + preview URLs snoozestack sites primary # serve this one at the project host root snoozestack sites list | delete [--yes] snoozestack domains claim # start connecting (idempotent) snoozestack domains status [] # the setup checklist snoozestack domains primary platform|custom # which URL generated links use snoozestack domains records list|add|delete … snoozestack domains list | retry | delete [--yes] ``` ```bash title="auth" snoozestack auth pull [--force] # write the email/page templates into the project as files ``` ```bash title="observe" snoozestack signals # hosted event stream, newest first snoozestack signals --range 24h --event page_view --lines 100 snoozestack signals tail [--type log] # the LOCAL dev store, live snoozestack signals query | clear # read / empty the local store snoozestack logs runtime --lines 100 # the runtime's log — auth, functions and storage are one process snoozestack logs runtime --preview # your preview's runtime instead of production's snoozestack logs queues | sites # queue and website activity snoozestack audit # who changed what — console, CLI, and MCP snoozestack audit --area auth --actor mcp ``` Function code lives at `./snoozestack/functions//index.ts` and table definitions in `./snoozestack/schema.mjs`. The login token is stored in `~/.snoozestack/config.json` (`APP_CONFIG_DIR` overrides it); CI should skip login entirely and pass a project token via `--token` or `APP_SERVICE_TOKEN`. `snoozestack update` upgrades the CLI in place. --- # Management API > The portal's own HTTP API — everything the CLI and UI do. [HTML version](https://snoozestack.com/docs/management-api/) · [Docs index](https://snoozestack.com/llms.txt) Everything the CLI and portal UI do goes through the portal's own HTTP API — there is no private back channel. Anything you can click, you can script. This page is for your own tooling. If the caller is an **AI client**, use the [MCP endpoint](https://snoozestack.com/docs/mcp.md) instead: it's these same operations exposed as MCP tools — with argument schemas, a project-scoped token you can rotate, and the docs served alongside them — rather than endpoints a model has to guess its way around. **Why use it** This is the **control plane**, and it's a different axis from the project API your app talks to. `/api/auth`, `/api/functions/v1`, `/api/storage/v1` and friends are what an app calls at runtime — none of them reach a table directly, a [function](https://snoozestack.com/docs/functions.md) has to sit in front; the management API instead creates and configures *the projects themselves*. Keep the two straight and the security model falls out naturally: app code holds a public project key and can never provision infrastructure, while a management token can provision anything and therefore never belongs in an app. Both of a project's keys sit on the app axis, the *service* key included. It exists so your own server can call this project's API — enqueueing to a [queue](https://snoozestack.com/docs/queues.md), say — and nothing more; it is not an admin credential, and no key of any kind reads a table. Anything administrative, reading rows with `snoozestack data` included, is this API with your login or a project token from `snoozestack projects tokens create`. Reach for it whenever a human clicking the portal is the bottleneck — when project setup has to be reproducible, or when the thing that needs to act isn't a person. - **Provision a project per customer** — A B2B product that gives every tenant an isolated database: create the project, publish its schema, and read back the keys, all from your signup flow, instead of clicking through the portal for each new account. - **Ephemeral environments in CI** — Spin up a throwaway project for a pull request, run the test suite against a real database and real functions rather than mocks, and delete it when the branch merges. (For most CI, `snoozestack dev` against the checkout is even simpler — no hosted project needed at all.) - **Infrastructure as code** — Keep secrets, scheduled functions, and DNS records in a script under version control, so a rebuilt project is identical to the one it replaced. - **Internal admin tooling** — Build your own support console — look up a user, ban an account, re-run a schedule — without giving support staff portal logins. ### Authenticating Sign the CLI in once — `snoozestack user login` opens your browser, and works for an account that signs in with Google as well as one with a password — then `snoozestack user token` prints the bearer token it stored in `~/.snoozestack/config.json`. Treat it like a root credential: it can read every key and delete every project in the organization. ```bash title="terminal" snoozestack user login # approve the code in the browser tab it opens TOKEN=$(snoozestack user token) curl -s https://snoozestack.com/api/projects -H "Authorization: Bearer $TOKEN" ``` A machine with no browser and no CLI can exchange an email and password for the same token at `/api/auth/cli-token` (`POST` `{"email", "password"}`) — for a password account only. For anything unattended, prefer a [project token](https://snoozestack.com/docs/cli.md) (`snoozestack projects tokens create`): it is scoped to one project and revocable from anywhere. A worked example — provision a project, wait for it, apply a schema, and print the keys an app would need: ```bash title="provision.sh" set -euo pipefail API=https://snoozestack.com/api AUTH="Authorization: Bearer $TOKEN" # 1. Create the project — the response is the project row, incl. its keys. REF=$(curl -s -X POST "$API/projects" \ -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"name": "tenant-acme"}' | jq -r .slug) # 2. Wait for the project's runtime to come up: runtime/state is 404 until # the starter publish that creation kicks off has a container answering. until curl -sf "$API/projects/$REF/runtime/state" -H "$AUTH" >/dev/null do sleep 3; done # 3. Apply the schema: the schema.json that "snoozestack schema build" writes # from schema.mjs, published as the project's new schema version. curl -s -X POST "$API/projects/$REF/schema" \ -H "$AUTH" -H 'Content-Type: application/json' \ -d "{"schemaJson": $(jq -Rs . < snoozestack/schema.json)}" # 4. Hand the app its credentials. There is no "url" field — a project is # always served at https://.snzzz.com. ANON=$(curl -s "$API/projects/$REF" -H "$AUTH" | jq -r .anon_key) echo "SNOOZESTACK_URL=https://$REF.snzzz.com/api" echo "SNOOZESTACK_ANON_KEY=$ANON" ``` ### Projects & schema | Endpoint | Methods | Purpose | | --- | --- | --- | | `/api/projects` | GET, POST | List / create projects | | `/api/projects/` | GET, DELETE | Project detail (incl. keys) / delete | | `/api/projects//pause` | POST | Stop the project: runtime removed, API and sites answer paused (owner only) | | `/api/projects//resume` | POST | Start it again, if the plan has a free slot | | `/api/projects//schema` | GET, POST | The project's current schema.json / upload a new one (what snoozestack publish sends) | | `/api/projects//runtime/state` | GET | What the running runtime holds: tables, functions, queues, sites — the console's read path | | `/api/projects//runtime/table/` | GET | One runtime table's rows, paged | | `/api/projects//schema/live` | GET | The schema production's database records as applied — what publish plans a release's drops against ({ schemaJson }, null before production has any) | | `/api/projects//schema/drop-impact` | GET | ?drop=
/ ?drop=
., repeated: rows per table and non-null values per column in production — what publish shows before a release drops data | | `/api/projects//schema/rebuild-impact` | GET | ?checks=: rows each rebuilt table keeps, values under a changed type, and the rows that would stop a rebuild (NULLs under a new NOT NULL, duplicates under a new unique key, orphans under a new foreign key) — counted in production before publish uploads anything | | `/api/projects//releases` | GET, POST | Releases / record one from a package manifest and deploy it to production (what snoozestack publish does last) | | `/api/projects//releases/` | GET | One release in full: manifest, checksums, schema plan, drops, diff, deployments | | `/api/projects//releases//deploy` | POST | Deploy a recorded release again (publish --release) — a rollback when it is older than the live one | | `/api/projects//deployments` | GET | Every publish and rollback, with actor, evidence and outcome | | `/api/projects//preview` | GET, POST, DELETE | The signed-in account's own preview: its name and status / deploy a package manifest to it (what snoozestack preview does last) / clean it up | | `/api/projects//preview/outbox` | GET | Mail your preview's runtime tried to send — captured, never delivered | | `/api/projects//previews` | GET | Everyone's previews of the project, with owner, URL, state and expiry | | `/api/projects//previews/` | GET, DELETE | One preview's status (probing it if still building) / clean it up now | | `/api/projects//previews//outbox` | GET | Mail the preview's runtime tried to send — captured, never delivered | ### Functions, schedules & secrets | Endpoint | Methods | Purpose | | --- | --- | --- | | `/api/projects//runtime/publish` | POST | Upload the whole snoozestack/ tree as a runtime version: dark for the live namespace (a release names it, its deploy activates it), live for a preview namespace | | `/api/projects//functions` | GET | The per-function store's listing, kept for the console's MCP page; what is live is runtime/state | | `/api/projects//functions/` | GET, PUT, DELETE | Read source / redeploy (merges files, preserves auth + MCP flags) / delete | | `/api/projects//schedules` | GET, POST, PATCH, DELETE | Cron schedules for functions — list / create / update / delete | | `/api/projects//schedules/run` | POST | Run a schedule immediately | | `/api/projects//secrets` | GET, POST, DELETE | Function secrets (applied live) | ### Auth | Endpoint | Methods | Purpose | | --- | --- | --- | | `/api/projects//auth/users` | GET, PATCH, DELETE | List users / ban-unban / delete — no POST: accounts exist only because someone signed up | | `/api/projects//auth/users/send` | POST | Send the sign-in link to an existing user: { type: "recovery", email } — it doubles as password reset. "invite" and "confirmation" answer 409: application identity has no such flows | | `/api/projects//auth/config` | GET, PUT | Site URL + additional redirect URLs | | `/api/projects//auth/providers` | POST, DELETE | Enable / disable Google and Apple sign-in | | `/api/projects//auth/providers/apple-secret` | POST | Sign an Apple client secret from the stored .p8 — returns it, saves nothing | | `/api/projects//auth/emails` | GET, PUT | Email template subjects and HTML bodies | | `/api/projects//auth/reset-config` | GET, PUT | Where the hosted password page hands off to instead of rendering its form | | `/api/projects//auth/reset-page` | GET, PUT | Hosted password page HTML | ### Storage & queues | Endpoint | Methods | Purpose | | --- | --- | --- | | `/api/projects//storage` | GET, POST, DELETE | Browse / upload / delete objects | | `/api/projects//storage/buckets` | GET, POST, PATCH, DELETE | List / create / toggle public-private / delete buckets | | `/api/projects//storage/sign` | GET | Mint a signed URL for a private object | | `/api/projects//queues` | GET, POST, DELETE | List (with message counts) / create / delete queues | | `/api/projects//queues//messages` | POST | Enqueue a message | | `/api/projects//queues/triggers` | GET, POST, PATCH, DELETE | Queue triggers — list / create / update (functions, enabled) / delete | | `/api/projects//queues/webhooks` | GET, POST, DELETE | Queue webhooks (public inbound URL → queue) — list / create / delete | ### Web & domains | Endpoint | Methods | Purpose | | --- | --- | --- | | `/api/projects//sites` | GET | List published static sites | | `/api/projects//sites/` | GET, POST, DELETE | Site manifest (paths + ETags) / upload a file / prune paths or delete the site | | `/api/projects//sites//publish` | POST | Turn staged uploads into a new, previewable version — not live until promoted | | `/api/projects//sites//promote` | POST | Make a version live and flush the edge cache — also how to roll back | | `/api/projects//sites//versions` | GET | Version history for a site | | `/api/projects//domains` | GET, POST, DELETE | List / connect / disconnect a domain | | `/api/projects//domains//records` | GET, POST, DELETE | List / add / delete DNS records | | `/api/projects//distributions` | GET, POST, DELETE | Attach / detach a custom domain to a site or the project API | ### Monitoring | Endpoint | Methods | Purpose | | --- | --- | --- | | `/api/projects//services/logs?service=…` | GET | The runtime's log (service=runtime; auth, storage and functions are aliases for it), or the portal-side queue and website activity (queues, sites) | | `/api/projects//audit` | GET | The project's change history — console, CLI, MCP and service tokens — newest first, filterable by area and actor | | `/api/projects//observability[?section=…]` | GET | Resources overview, or per-product detail (database, auth, storage, functions, queues, sites) | | `/api/projects//signals?range=…` | GET | Signals overview aggregates | | `/api/projects//signals/events` | GET | Raw event feed, newest first, paginated | | `/api/projects//signals/users` | GET | User profiles derived from events | | `/api/projects//signals/meta` | GET | Event names / property keys / top property values in a range | ### Organization | Endpoint | Methods | Purpose | | --- | --- | --- | | `/api/org` | GET, PATCH | Organization detail / rename | | `/api/org/members` | GET, POST, DELETE | Admin accounts — list / invite / remove | | `/api/org/audit` | GET | The organization-wide change history | | `/api/projects//mcp-tokens` | GET, POST | MCP access tokens — list / create (DELETE on /, POST //rotate) | | `/api/projects//service-tokens` | GET, POST | Project tokens for CI — list / create (DELETE on /, POST //rotate) | Responses are JSON; failures carry a non-2xx status and an `{ "error": "…" }` body. See the [CLI reference](https://snoozestack.com/docs/cli.md) for the commands that wrap the most common of these calls.