# Remote Proxy / Satellite Paskia can serve a configured domain (rp-id) from a **remote** paskia instance instead of the local database, so latency-sensitive checks (`/auth/api/forward`, `/auth/api/validate`) answer in ~1 ms even when the auth server is on another continent. Example: `app2.example.com` runs on our local host and needs fast local checks, while `app1.example.com` and `auth.example.com` run far away — all sharing `example.com` as rp-id. Client applications that used `https://auth.example.com` as their auth backend (forward-auth checks) only repoint to the local satellite (`http://127.0.0.1:4401`); both remain usable interchangeably, and the satellite ultimately uses `auth.example.com`. Status: **implemented** (see `paskia/satellite.py`, `paskia/syncfeed.py`, `paskia/fastapi/sync.py`, `paskia/fastapi/proxy.py`). The design review comparing the rejected alternatives is at the end of this document. ## Configuration A domain becomes remote through its stored domain config (admin domains API; the frontend form may lag): ```json "remote": { "url": "https://auth.example.com", "token": "", "cache_ttl": 60, "refresh_interval": 300 } ``` - `url` — the remote instance's base URL. The satellite connects to `{url}/auth/api/sync/ws`; the connection is server-to-server and not host-dispatched, so internal addresses work. - `token` — bearer token for the sync channel. The **remote accepts tokens via its `PASKIA_SYNC_TOKENS` environment variable** (comma-separated); nothing is stored in the remote's database, and with the variable unset the sync endpoint stays closed. The token is write-only over the admin API (never echoed back). - `cache_ttl` — seconds the replica remains trusted after the sync channel goes down; then the satellite fails closed (503). Set it large (up to the 24 h session lifetime) for fail-open behavior. - `refresh_interval` — seconds between full snapshots (reconciliation); reconnects in between replay missed events from the remote's RAM ring buffer. A remote domain **must mark an auth host** (validated cross-domain): the profile, admin and sign-in pages live there, so browsers and WebSockets go directly to the remote. Other domains on the same satellite remain fully local — the multi-domain config mixes both kinds freely. ## What the satellite holds A RAM-only read replica of the remote's tables (permissions, orgs, roles, users, credentials, sessions) as another plain `DB` struct instance, attached to the runtime `Domain` as its `store`. It is never persisted, rebuilt from a snapshot on startup, kept current by sequenced events over the sync WebSocket, and swept for expired sessions locally. The feed carries no usable secrets: sessions are keyed by `hash_secret` output, credentials carry public keys only, and the OIDC signing key is never replicated. Reads run unchanged against the replica: `DB.session_ctx` and the verify/`/check`/`/user-info` helpers take an explicit `store` (the dispatched domain's), defaulting to the local database. There is no context-dependent global accessor. ## Endpoint behavior for remote domains | Endpoint | Handling | |---|---| | `GET /auth/api/forward`, `GET /check`, `GET /user-info`, `GET /settings` | served from the replica (sub-ms) | | `POST /auth/api/validate` | verified from the replica; the throttled refresh updates the replica and is written back over the sync channel; cookie renewed locally | | `POST /auth/api/logout` | proxied to the remote (original Host preserved) and evicted from the replica immediately | | `POST /auth/api/set-session`, `GET /token-info` | proxied (the exchange code/reset token lives in the remote's RAM/DB); the session arrives via sync event | | `/auth/oidc/*` | proxied (signing key and OIDC sessions stay on the remote) | | `/auth/ws/*`, `/auth/remote-auth/*`, admin, profile | not served — the auth host requirement means these are reached on the remote directly | Freshness hierarchy: 1. Changes made **through** the satellite: immediate (write-behind, optimistic eviction). 2. Changes made **directly on the remote**: a sync event, ~1 network RTT. 3. Channel down: the replica stays authoritative until `cache_ttl` past the disconnect, then 503. On reconnect, missed events are replayed from the remote's ring buffer, or a full snapshot is taken (always at `refresh_interval` and after remote restarts, detected via a generation stamp). ## The remote side Strictly additive and RAM-only: a `syncfeed` ring buffer fed by hooks in the struct `store()`/`delete()` methods (which also cover cascade deletes) plus explicit emits for field-mutating operations, and the token-gated `/auth/api/sync/ws` endpoint serving snapshots, event replay and live events, and accepting `session_refresh` write-backs. With no satellites connected, the hooks are a no-op. ## Trust and caveats - The satellite host holds a full copy of the remote's auth data in RAM (minus the OIDC key) — treat it as trusted as the remote. - Disconnect detection is bounded by the sync keepalive (~10 s) plus `cache_ttl`. - Avatars are stored on the remote's disk; `user-info` from a replica reports no avatar URL. - OIDC sessions in a replica-backed `user-info` show the client UUID rather than its name (OIDC clients are not replicated). - Remote and satellite should run compatible versions; the sync handshake carries a generation stamp and protocol mismatches fall back to snapshots. --- # Design review (the rejected alternative) ## Option A — caching HTTP reverse proxy A thin proxy caching `/auth/api/forward`, `/check`, `/user-info`, `/settings` responses keyed by `(Host, cookie, query)` with `TTL = min(configured TTL, Remote-Session-Expires − now)`; everything else forwarded verbatim, WebSockets tunneled, `/logout` intercepted for eviction. **Option B** adds a remote change feed so eviction happens within one RTT instead of at TTL. - Remote changes: none for A; one additive endpoint for B. - The proxy needs no credentials — requests are authenticated by the end user's cookie, forwarded on a miss. ## What the read-only local state buys over the HTTP cache - **Full `SessionContext` locally.** A replays the byte-response it once saw; D *computes* the answer. Query combinations never seen before (new `perm`/`max_age`/`public` shapes) are served locally by D but miss A's cache. D caches the *domain model*, so derived answers (effective permissions per host, `max_age` against `credential.last_used`, `Remote-*` composition) are correct without having been witnessed. - **One invalidation model.** A hand-builds invalidation rules per endpoint (query-key mapping, cookie re-keying on renew, 401 variants). D's events mutate the replica (upsert/delete by table+key) and every endpoint becomes consistent at once — including future ones. - **Degradation behaves like a real instance.** With the remote down, D serves a coherent auth service from the replica (expiry enforced locally, bounded by `cache_ttl`); A serves unrelated cached responses with gaps wherever the cache was cold. - **Multi-domain uniformity.** D is a property of a domain in the existing registry; local and remote rp-ids coexist in one instance. A is a separate component bolted in front of specific URLs. - **User simplicity.** D is configured once in domain config; A needs deployment and cache-key discipline per frontend application. ## What D costs - A store-explicitness refactor in the security-critical read path (`DB.session_ctx` self-contained; explicit `store` parameters) — small but review-worthy. (The first draft's contextvar-dependent `db.data()` was rejected: a global accessor whose meaning shifts under the caller.) - A versioned sync protocol (snapshot + sequenced events + ring-buffer replay + reconnect reconciliation). - A trusted satellite host (full data copy in RAM). - Additive remote code (sync endpoint + commit hooks), where A needs none. - Replica housekeeping (expiry sweeper, write-behind ordering, optimistic eviction vs. event confirmation). ## Summary A(+B) is the right tool to "make forward-auth fast in front of an untouched server". D — implemented here — is the right tool when the satellite should *be* a paskia instance for its remote domains: one consistency model, correct answers for un-cached query shapes, graceful degradation, and per-domain mixing with local rp-ids, at the price of the core refactor, the sync protocol, and a trusted satellite host.