Defense in depth across two independent authentication planes. polyrouter is metadata-only by default — your prompts and responses are never stored unless you explicitly enable encrypted body capture (self-host only, off by default). Credentials are encrypted at rest, egress is SSRF-guarded, and every query is scoped to its tenant.
Maintained by Anthony Izzo · Last updated
By design, polyrouter never stores prompt or response bodies by default. Every request writes one immutable request_log row of metadata — enough to price, route, and debug, and nothing that reveals what was said.
Self-hosted instances may turn on prompt/response body capture from the dashboard Settings page — for debugging or audit — behind an explicit consent gate. It is off by default and unavailable on cloud instances (MODE=selfhosted only).
| Mode | What it captures |
|---|---|
| offdefault | Nothing — no bodies are ever written. |
| errors_only | Prompt and response for requests that error — and for requests the cascade escalated (an escalation is a successful outcome). The dashboard labels this mode “Errors & escalations only”. |
| all | Prompt and response for every request. |
Two independent authentication systems that never share a code path: browser sessions for the dashboard (/api/**) and HMAC-signed API keys for agents (/v1/**). Four routes sit outside both by design: GET /api/health for orchestration probes; GET /api/login-config, which the sign-in screen reads before anyone has a session; POST /api/invites/accept, which is how an invited user gets their first session; and GET /metrics, which serves instance-level Prometheus aggregates to anyone who can reach the port — set METRICS_ENABLED=false to 404 it, and keep the port behind a reverse proxy.
The first account to sign up owns the instance; everything after that is invite-only and admin-managed.
Every server-fetched URL or host — provider base URLs, subscription OAuth token endpoints, Apprise notification targets, SMTP hosts, and the pricing-refresh URL — is resolved and checked before any connection. assertUrlSafe() throws on a private, loopback, link-local, CGNAT, or cloud-metadata target, IPv4 and IPv6 alike.
| Range | Classification | Blocked |
|---|---|---|
| 10.0.0.0/8 | Private † | Blocked |
| 172.16.0.0/12 | Private † | Blocked |
| 192.168.0.0/16 | Private † | Blocked |
| 127.0.0.0/8 | Loopback | Blocked* |
| 169.254.0.0/16 | Link-local | Blocked |
| 100.64.0.0/10 | CGNAT † | Blocked |
| fc00::/7 | Unique-local (IPv6) † | Blocked |
| Metadata endpoints | AWS / GCP / Azure | Blocked |
Provider and notification credentials are encrypted at rest with AES-256-GCM. They are decrypted only at call time, and adapters are built lazily per attempt, so an unused provider's secret is never decrypted. Credential content is never logged or echoed.
Each stored provider credential is a typed envelope — polycred:v1: — that is either a plain API key or an oauth subscription token. Plain writes wrap whatever is pasted, so a polycred:v1:… lookalike just becomes a plain value containing that string. The oauth kind is unforgeable through every paste path — only the connect / refresh flow can mint one. Marker-prefixed content that fails to parse raises a typed TamperedCredentialError, never a silent downgrade to plain.
| Secret | Env var | Purpose |
|---|---|---|
| Auth secret | BETTER_AUTH_SECRET | Dashboard session signing |
| API-key HMAC | API_KEY_HMAC_SECRET | Agent-key hashing, plus a domain-separated per-tenant learning HMAC |
| Credential key | PROVIDER_CREDENTIAL_KEY | Provider secrets (plain keys + OAuth envelopes) and captured bodies |
| Notification key | NOTIFY_CREDENTIALS_SECRET | Notification channel credentials |
Every owned table carries an owner_user_id, and every query is scoped to the authenticated principal. The Principal type makes an unscoped query a compile error — the single function that derives the ownership predicate demands one, and every scoped repository funnels through it — and a dedicated cross-tenant e2e suite verifies isolation across every endpoint.
// THE single site deriving the ownership predicate
function ownershipPredicate(
table: { ownerUserId: AnyPgColumn },
principal: Principal,
): SQL {
assertUserPrincipal(principal);
return eq(table.ownerUserId, principal.userId);
}
Foreign keys with ON DELETE CASCADE clean up a deleted tenant's data (the last enabled admin excepted). The Layer 2 learning sweep inherits the same tenancy: its per-tenant Redis buckets are keyed by a domain-separated tenantHmac, so one tenant can neither read nor pollute another's state.
The optional semantic router (Layer 2) is the only surface that touches request text, so its privacy contract is explicit and test-enforced. It does two jobs from one bounded embedding per request — the band verdict, and the research / writing workload classes — never a second embed for the pair. Eight invariants hold at both the application layer and in the suites.
| # | Invariant | Enforced by |
|---|---|---|
| 1 | No telemetry for skip verdicts — a populated semantic column always came from a real evaluation. | All-or-none quartet DB CHECK |
| 2 | No raw embedding in any column, log line, metric, or API response. | Ephemeral Float32Array, dropped after use |
| 3 | No single raw embedding in Redis — only a sum over a cohort of at least SEMANTIC_LEARNING_MIN_COHORT (default 8). | Accumulator flush floor |
| 4 | No prompt text in an audit row — scalars only (counts, drift, similarity) and a fixed reason string. | semantic_learning_event column types |
| 5 | No “learned” badge when stale — a moved embedder or revision shows source: bundled. | View-model + unit tests |
| 6 | Revert fences everything — the revocation epoch bumps in Postgres before Redis is cleared, so every in-flight sweep's CAS fails. | Ordered revert + e2e test |
| 7 | Honest availability — an opted-in bundle that fails to load fails boot naming the variable and reason, so the layer is never silently inert. Where the embedder is simply absent (or its anchors do not separate) the toggle greys instead (semanticAvailable: false) and the verdict never runs. | Bootstrap capability check |
| 8 | A workload verdict is numbers only — the class, a confidence in [0, 1], the source, and a configuration digest computed at boot. No prompt text, no hash of it, and no vector reaches a column, the routing reason, or a log line. | All-or-none quartet DB CHECK + boot-computed revision |
An internal 19-surface multi-agent audit was run on 2026-07-16 against commit 8abd4b6, covering every file in packages/ plus the root operational files. It confirmed 0 critical, 9 high and ~37 medium findings, each survivor of an adversarial verification pass; all have since been resolved. The report itself is an internal working document and is not published; the fixes it produced are in the public history. It predates the Layer 2 semantic stack, which ships its own dedicated suites.