# {{capProjectName}} {{capAppName}} — status-page backend

The backend for a public **status page**: live queries anyone can subscribe to,
plus operator-only mutations to declare and resolve incidents. Boots
**zero-infra** (`store: 'memory'`). Pair it with the **frontend-status** web
template:

```bash
voltro init my-status --api=api-status --web=frontend-status
voltro dev
```

## The asymmetry it encodes

A status page is read by everyone and written by a few — so:

- **Reads are PUBLIC.** `incidents.live`, `updates.list`, `components.list` each
  declare `openAccess: '<why>'`, so an anonymous browser can
  `useSubscription('app', 'incidents.live')` and the timeline updates live.
- **Writes are OPERATOR-ONLY.** Every mutation declares
  `guards: [{ scope: 'status:write' }]`, enforced by `@voltro/plugin-rbac` in the
  dispatch spine before the executor. The demo resolver (`authz.ts`) grants the
  `operator` role to the `ops` tenant; everyone else — anonymous included — is
  denied.

Note that **both** of those are DECISIONS, and the framework requires one or the
other: under `security.defaultDeny` (on by default) a wire-exposed procedure
that declares neither `guards:` nor `openAccess:` refuses to boot. `openAccess`
is not the lax option — it is the honest spelling of an open endpoint, and the
reason it carries is what a reviewer reads and what `voltro doctor` prints. This
template is the clearest place in the library to see the two side by side.

## Surface

| Tag | Kind | Guard |
|---|---|---|
| `incidents.live` | query (stream) | — public |
| `updates.list` | query (stream) | — public |
| `components.list` | query (stream) | — public |
| `incidents.create` | mutation | `status:write` |
| `incidents.update` | mutation | `status:write` |
| `incidents.resolve` | mutation | `status:write` |
| `components.create` | mutation | `status:write` |

## Try it (curl)

```bash
voltro dev .

# A public read — no auth (anyone can watch)
curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
  -H 'content-type: application/json' \
  -d '{"tag":"incidents.live","input":{}}'

# An operator write — needs the `ops` tenant (demo resolver → operator role)
curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
  -H 'content-type: application/json' -H 'x-tenant: ops' \
  -d '{"tag":"incidents.create","input":{"title":"API degraded","impact":"major"}}'

# The SAME write without the operator tenant → typed ScopeError
curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
  -H 'content-type: application/json' \
  -d '{"tag":"incidents.create","input":{"title":"nope","impact":"minor"}}'
# → { "ok": false, "error": { "_tag": "ScopeError", "required": "status:write" } }
```

## Files

```
app.config.ts             rbacPlugin (roles + demo resolver)
authz.ts                  the role→scope map (status:write) — one source of truth
database/schema.ts        components / incidents / incident_updates (PUBLIC, not tenant-scoped)
queries/                  incidents.live / updates.list / components.list — ungated streams
mutations/                incidents.create|update|resolve, components.create — status:write
tests/                    the public-read / operator-write asymmetry + descriptor pins
```

## Going to production

- **Real operator auth.** Replace the demo `resolveRoles` with subject metadata
  or a DB lookup, and pair this api with `api-auth` / `api-saas-starter` so
  operators actually sign in. The rbac gate is already in place — you only swap
  where the roles come from.
- **Durable store.** `store: 'postgres'` (or `sqlite`) so incident history
  survives a restart.
- **Notify subscribers** on open/resolve with `@voltro/plugin-mail` (opt-in).

## Anti-patterns

- **Tenant-scoping the public tables.** Adding `tenant()` to `incidents` would
  make an anonymous visitor (no tenant) see nothing. Public reads stay
  unscoped; the rbac guard on the writes is the boundary.
- **A write-only authz model.** These reads are intentionally ungated because
  they're public — but if you ever gate a read, gate it with `guards:` on the
  query (enforced on open AND every delivery), not a check inside the handler.
