Data boundary
Where content travels — and what leaves your environment: metadata only.
- Billing, connectors, spend rows, and logs contain metadata only.
- Trace content follows
ANYRAY_CONTENT_MODE.
How the boundary is enforced
Six independent layers, so one misconfiguration doesn't expose content:
| Layer | Guards against | Control |
|---|---|---|
| Encryption at rest | Content readable on disk | ANYRAY_CONTENT_KEY |
| Content mode & fail-safe | Content being kept at all, or silently escalating | ANYRAY_CONTENT_MODE, ANYRAY_ALLOW_PLAINTEXT |
| Content-free spend store | Content leaking into governance data | Metadata-only by design |
| Trace export gating | Content leaving in exported traces | ANYRAY_CONTENT_MODE |
| Human surfaces | Humans seeing content in the UI | ANYRAY_CONTENT_MODE + RBAC |
| Admin access control | Unauthorized access to data and settings | ANYRAY_ADMIN_TOKEN (+ optional SSO / RBAC) |
Encryption at rest
In encrypted mode (the default), content is encrypted with AES-256-GCM before it touches disk, using ANYRAY_CONTENT_KEY (32 hex bytes — openssl rand -hex 32). It fails closed: no plaintext fallback, and the gateway refuses to boot without a valid key. Decryption is an offline, authorized step — the console never decrypts content for display.
Content mode & fail-safe
One org-wide mode, ANYRAY_CONTENT_MODE (default encrypted), decides how much content is kept. Change it at runtime on the console Privacy page; every change is audit-logged with the actor and before/after value.
| Mode | Behavior |
|---|---|
encrypted (default) | Content stored as ciphertext (AES-256-GCM via ANYRAY_CONTENT_KEY). |
off | No content stored — nothing to decrypt. |
plaintext | Raw content, readable by admins. Requires ANYRAY_ALLOW_PLAINTEXT=true; debugging only. |
The mode only ever degrades toward off, never escalates: without the gate, a plaintext setting falls back to encrypted (if a key is set) or off. See Configure.
Content-free spend store
For spend governance, the gateway writes one metadata row per request — user/team (from x-anyray-metadata), model, provider, token counts (with the prompt-cache split), cost, savings, latency/status. A request routed through a custom provider host is flagged with a boolean (via_custom_route) only — the hostname itself is never stored. No field carries content, not even ciphertext. Rows are retained per ANYRAY_SPEND_RETENTION_DAYS (unset = kept indefinitely).
The same Postgres holds anyray_auth_events — content-free, reason-coded auth/cap rejections for the admin Diagnose view, retained 30 days, never sent to Anyray.
Configured spend connectors write vendor metadata to local
anyray_spend_connector_events. These rows never affect anyray_spend, token caps, request
governance, or Billing rollups. They expire after 90 days, or sooner when
ANYRAY_SPEND_RETENTION_DAYS is shorter, and owner-level GDPR endpoints cover them. Content fields
are never imported.
What the Billing app rollup sends
Every deployment sends a content-free Billing rollup every 15 minutes and after health changes. It never includes prompt or response content. The rollup contains:
- Seats — a distinct active-seat count plus opaque seat hashes (salted pseudonyms; the salt never leaves your deployment).
- Usage totals — requests, tokens, cost, and month-to-date savings, bucketed by model / provider.
- Deployment heartbeat — versions, uptime, per-leg health, request/error counts (error class names only), content mode, update method, auto-soft-update state, replica hostname, a count of health reports per
anyray-connectversion in use, and the names of setANYRAY_*env vars — values never cross. - Optimizer health — hour-by-hour counters per model: optimize outcomes (applied / skipped / timeout / error and normalized reason tokens), hook latency percentiles, per-strategy fired counts and tokens saved, and upstream provider failure counts (429/529/4xx/5xx). Counters and enum tokens only — no per-request data, no user field of any kind.
This powers Anyray-side fleet support ("your deployment has been unhealthy since 09:12") with no access to your environment. Privacy → Data shared with Anyray sets the heartbeat tier (standard default, floor for the mandatory core) and shows the literal last payload that crossed.
ANYRAY_METER_PER_USER=true adds a per-user spend + savings breakdown (counts and dollars only, still content-free). Leave it unset to keep the rollup fully pseudonymized.
Optional vendor connector polling
Connectors remain off until an admin adds a credential. In single-tenant mode, security_admin or
owner may configure them; in fleet mode, only the deployment owner may do so. The gateway then
polls these hosts:
| Connector | Outbound HTTPS host |
|---|---|
| Cursor | api.cursor.com |
| Devin | api.devin.ai |
| GitHub Copilot | api.github.com, then GitHub-issued report downloads pinned to copilot-reports.github.com |
Calls send the credential and report window; responses contain metadata stored locally. No inference content is sent. Remove the credential to stop polling.
Support bundle (customer-initiated)
A separate, optional egress for escalation. An admin generates the bundle, reviews the redacted JSON on screen, then separately clicks Send — the gateway regenerates it at send time, so the server, not the browser, decides what leaves. The egress is audit-logged and can never be triggered or pulled by Anyray.
Trace export gating
Traces persist to Postgres inside your environment (anyray_traces / anyray_observations). Content is gated by the mode before it is written: ciphertext in encrypted, omitted in off, raw only in deploy-gated plaintext. Tokens, cost, provider, and attribution are always plain metadata. Traces are pruned at boot and daily — rows older than 90 days (or ANYRAY_SPEND_RETENTION_DAYS) are deleted.
Durable context stash
output_externalize (off by default) can move a bulky tool output off the request and serve it back on demand; observation_mask stashes masked observations the same way. Originals are held in memory by default; the opt-in durable tier persists them in your spend Postgres (anyray_context_stash), gated like the trace store — an AES-256-GCM blob in encrypted, nothing in off. The handle is a content-free keyed HMAC, recall is scoped to the stashing user, and expired rows are swept hourly.
Human surfaces
- The live
/log/streamview shows[REDACTED]for content fields in every mode; request logs are metadata-only even when trace capture is deploy-gatedplaintext. - Console list views drop request/response bodies entirely.
- The trace detail view opens to any role with
observability:read(viewer+) and shows metadata — model, tokens, latency, cost, optimizer decisions.
Seeing the bodies inside a trace requires observability:read-content (security_admin and owner only); every other role gets a redacted response, not a 403. What they then see still follows the mode: ciphertext in encrypted, nothing in off, readable only in plaintext.
Admin access control
The console and every /admin/* endpoint sit behind admin auth — the break-glass ANYRAY_ADMIN_TOKEN (compared in constant time) and, optionally, per-operator console SSO with RBAC. See Security → Console access and RBAC.