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