How we protect your API keys, your prompts, and your account. If you find a vulnerability, we'd rather you tell us than the internet.
Your BYOK provider keys are encrypted at rest using AES-256-GCM. Each row gets a fresh 96-bit IV and an authentication tag — both stored alongside the ciphertext so a single corrupted byte fails closed instead of silently decrypting to garbage.
The master key is held in a server-only environment variable (BYOK_MASTER_KEY), SHA-256-derived to a 32-byte AES key at process boot, and never logged. In production we refuse to start without it; the dev fallback is gated on NODE_ENV !== "production". Plaintext keys are decrypted in memory at the moment of forwarding a request to the upstream provider, then released to the GC at the end of the request. They are never written to logs, Sentry traces, or support tickets.
All public network transport is TLS 1.3, terminated by our Caddy reverse-proxy with certificates auto-provisioned from Let's Encrypt. HSTS is enforced with a 1-year max-age, includeSubDomains, and preload.
We use application-layer scoping for every user-owned record. Every read or write in lib/queries.tsfilters by the authenticated session's user ID before returning rows; the session itself is an iron-session sealed cookie (HttpOnly, Secure, SameSite=Lax) signed with a server-only secret. SQLite enforces foreign-key constraints at the storage layer so cross-user references can't be fabricated.
Sensitive mutations (auth events, BYOK key add/remove, API key issue/revoke, account export, account deletion, admin actions, payment events) write to an append-only audit log that's reviewable internally and exposed to the affected user via account export.
Internal admin access is gated by an admin role on the user record, surfaced through /admin/* pages that re-check the role on every request. Admin actions are themselves audit-logged.
Oort runs as a single Docker Compose stack on a DigitalOcean droplet. Caddy fronts the app, terminates TLS, and serves long-lived Cache-Control on static assets. The application is a Next.js process talking to a local SQLite database in WAL mode; SQLite is the system of record. Outbound provider calls (OpenAI, Anthropic, Cerebras) leave the droplet only at the moment of a user-initiated run.
We do not run a separate Redis, Postgres, or vault service. That keeps the trust surface small — every byte of data lives on disks we control, encrypted by the same scheme described above.
Automated off-site backups are on our near-term production roadmap; until that ships, recovery procedures are documented and rehearsed manually. We'll update this page the day the cron goes live.
When you add an API key:
When you delete a key, the encrypted row is removed immediately. There is no soft-delete on BYOK rows: once the row is gone, the ciphertext is gone, and the auth tag and IV are gone with it. The plaintext was never persisted.
We maintain a documented incident response runbook with defined severity tiers. Our commitments:
A dedicated public status page is on our roadmap; until it ships, incident notices live here and on the home page.
Oort is designed around the following frameworks. Where an external attestation isn't yet in hand, we say so — we don't claim certifications we don't have.
We do not currently hold HIPAA, PCI-DSS, or FedRAMP certifications. Don't put regulated health data, payment-card data, or federal data into Oort.
If you find a security issue, please report it to security@oort.dev. Include steps to reproduce, the affected endpoints, and any proof-of-concept you have.
We commit to:
A formal bug-bounty payout schedule is in development. In the meantime we evaluate each valid report case-by-case and will negotiate a reward for severity-warranted findings; please don't publicly disclose issues before we've had time to patch them.
Security inquiries, attestation requests, and vulnerability reports all go to security@oort.dev. PGP key available on request.