# Changelog 0.9

## v0.9.1 — 2026-08-04

### 💥 Breaking Changes

- `otp()` option rename (pre-1.0, no compat shim): `tier: 1 | 2` → `mode: "provider" | "app"`. Plug-time / boot-time errors, plugin config snapshot, and docs use the new names. Docs also state that the two modes cannot both be active (same fixed `/auth/otp/*` routes — combine with a different plugin such as `magicLink()` instead), and clarify that `fx.sendOtp` / `fx.verifyOtp` are raw Channel capabilities (no `.plug()`) while `otp()` in provider mode is the full auth feature on top of them.

## v0.9.0 — 2026-08-04

### ✨ Added

- Redis-protocol `store.kv` image recipes — Redis (default), Valkey (`valkey/valkey`), and Dragonfly (`docker.dragonflydb.io/dragonflydb/dragonfly`). Driver id stays `redis`; pin via `images["store.kv"]`. Docs list each license in one line (RSAL/SSPL/AGPL · BSD-3 · BSL) and note that service-resale clauses only matter if you offer that datastore as a service — not for typical self-hosting.

- `compose.all.yml` — fully merged compose (layers 1–3) emitted alongside the existing per-role files; for Swarm `docker stack deploy -c compose.all.yml` and anyone who prefers one file. Layered `-f` order unchanged.

- PgDog as the default docker/prod SQL connection pooler — sits in front of Postgres (`images.pgdog`), transaction pooling, `DATABASE_URL` → `:6432` when both are pinned. Caps `N × pool` vs Postgres `max_connections` at the infrastructure layer (no in-process pool throttling). Read-replica routing (`BEGIN READ ONLY` → replica, failover on promotion) documented as readiness; not configured this round.

- `otp()` plugin — unified multi-channel OTP replacing `emailOtp()` / `phoneNumber()`. Explicit `tier: 1` (provider Verify via unchanged `fx.sendOtp`/`fx.verifyOtp`) or `tier: 2` (app-owned codes over declared `channels`: SMS / WhatsApp / email). Tier 2 adds channel-neutral resend (`POST /auth/otp/resend`, 60s cooldown), sealed OTP copy (HKDF `oke-otp-seal-v1` + AES-GCM, wiped with challenge TTL), and `fx.deliverOtp` (sently `FallbackTransport` cross-medium failover; Taqnyat WhatsApp `sendWithFailover` when bound).

- `taqnyat-whatsapp` Channel driver + `drivers.channel.whatsapp` boot bind (`wa-cloud` also wired).

- `GET /_/ready` — kernel readiness (`booting` → `orphan_scan` → `ready`) distinct from app `GET /health` liveness; `503` until the durable orphan scan finishes.

- `installGracefulShutdown` / `releaseInstanceLeases` — SIGTERM/SIGINT releases Clock + Journal leases held by this instance (reuses existing `releaseLease`), then drains the server. Wired on `oke dev`’s app runner.

- Kubernetes guidance — plain **Deployment** (never StatefulSet), probes, shared drivers, and honest multi-instance known limits (`site/content/docs/deployment/kubernetes.mdx`).

- Docker Swarm guidance — `docker stack deploy` on the generated compose layers; `compose.prod.yml` app `HEALTHCHECK` → `GET /_/ready`, `deploy.update_config` / `restart_policy`, `stop_grace_period: 30s`; `compose.yml` emits `app.image` for Swarm (ignores `build`) (`site/content/docs/deployment/docker-swarm.mdx`).

- Opt-in Docker proxy recipes — `images.proxy` with **Caddy** (automatic HTTPS via generated `Caddyfile`) or **Traefik** (Docker-label auto-discovery for `docker compose up --scale app=N`). Default remains no proxy (app publishes `6530`). Traefik mounts the Engine API only through `tecnativa/docker-socket-proxy` (never raw `docker.sock` on the edge). Docs: `site/content/docs/deployment/reverse-proxy.mdx`.

- Deployment docs section — `site/content/docs/deployment/` (compose vs Swarm vs Kubernetes decision hub + self-contained path pages for Docker / Swarm / Kubernetes: PgDog, SIGTERM, readiness, Signal/Channel honesty, reverse-proxy option in full on each).

- Horizontal multi-process integration test — two OS processes sharing live Postgres + Redis prove Clock-once, durable takeover, shared Gate rates, and mid-scenario SIGKILL absorption together (`OKE_TEST_POSTGRES_URL` + `OKE_TEST_REDIS_URL` / `REDIS_URL`; visible skip when unset).

### 💥 Breaking Changes

- `emailOtp()` and `phoneNumber()` plugins — use `otp({ tier, … })` instead (pre-1.0; no compat shims).

### ♻️ Changed

- Deployment docs promoted to a top-level sidebar section (`site/content/docs/deployment/`) — peer of Elements / Console / Reference; Get Started is first-five-minutes only again.

- `sently` → `1.2.1` — Taqnyat Verify OTP live-verified end-to-end (`sendOtp` → handset code → `verifyOtp`); transport reads `Data.result` from live `returnJson` envelopes (fixes false `code 1` on success).

- Gate `drivers.store.kv: redis` without `REDIS_URL` / inject **fails loud** (no soft-fallback to memory that silently doubles rate budgets).

- Signal `redis` docs + boot warn: emit relays to Redis; consume / `live` / `drain` remain process-local outbox until Streams consume ships.

- Channel docs + boot warn: default suppression / consent / receipts are process-local until a durable driver ships (deferred epic — documented, not silent).

- Store `files: "fs"` boot warn — single-host; horizontal scale needs `s3`.

### 🐛 Fixed

- Session scopes / audience live on the session row (and Console SQLite hydrate/persist) — refresh no longer loses them across process restart via module-level Maps.

- App-plane sign-in rate limit relies on shared Gate KV only (removed the process-local email attempt bag that bypassed redis rates).
