# {{projectName}} / {{appName}}

Voltro API scaffold (template: **api-ratelimit**) — per-endpoint /
per-subject / per-tenant rate limiting with `@voltro/plugin-ratelimit`.

## Boot

```bash
pnpm install
pnpm --filter @{{projectName}}/{{appName}} dev
# → http://localhost:4000  (store: memory — zero infra)
```

## What this shows

- **Limits as config** — `rateLimitPlugin` runs on the rpc interceptors (the
  only surface that sees every call AND the resolved subject). A `default`
  fallback applies everywhere; `rules` override per endpoint:
  - `match` — an exact tag or a `/regex/`; `kind` narrows to mutation/query/action.
  - `algorithm` — `sliding-window` (default), `fixed-window`, or
    `token-bucket` (with `burst`).
  - `by` — the bucket key: `subject` (default), `tenant`, `apiKey`, `global`,
    or a composite array.
- **Typed `RateLimited`** — over-limit calls fail a `RateLimited` error
  (`{ tag, limit, retryAfterMs, resetAtMs }`). The plugin merges it into every
  procedure's wire error union, so the client decodes it typed and can show a
  "try again in N seconds".

This template caps `notes.create` at **3/min per tenant** (token-bucket,
burst 3): the first three calls land, the fourth is rejected.

## Try it

```bash
# Fire it four times for tenant 'acme' — the 4th trips the limit:
for i in 1 2 3 4; do
  curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
    -d "{\"tag\":\"notes.create\",\"input\":{\"tenantId\":\"acme\",\"title\":\"n$i\",\"body\":\"x\"}}" \
    | python3 -c 'import sys,json;d=json.load(sys.stdin);print(i, d.get("ok"), (d.get("error") or {}).get("name",""))' i=$i 2>/dev/null || true
done
# → 1 True ; 2 True ; 3 True ; 4 False RateLimited
```

## Multi-node

`store: 'memory'` is single-process. For a cluster, switch to `'postgres'`
(state under a row lock, reuses `DB_*`/`PG_*`) or `'redis'` (fastest — one
atomic Lua script; reads `CACHE_REDIS_URL` / `REDIS_URL`). All shared stores
fail OPEN on a backend error.
