The architecture we'd want if we were the agency holding the keys.
Flarewatch holds client cloud credentials on behalf of agencies. This page documents how — and what we'd do if something went wrong.
Threat model — who we defend against.
The asset is the set of BYO provider credentials an agency stores so Flarewatch can read traffic, revenue, and email-health data from Cloudflare, Supabase, Stripe, and Resend.
We defend against four realistic adversaries: (1) an external attacker who compromises a Flarewatch operator account; (2) an external attacker who compromises Supabase or Cloudflare infrastructure beneath us; (3) a malicious or curious Flarewatch employee (we are currently zero employees, but the design assumes we won't be forever); (4) a tenant attempting to escalate across other tenants' data.
We do not claim to defend against a nation-state with physical access to a tenant's laptop. We also do not defend against an agency that stores a Stripe secret key — which is why we refuse to accept one (restricted-read only).
The vault — BYO keys, envelope encryption.
Every tenant gets a per-tenant Data Encryption Key (DEK, AES-256-GCM). The DEK encrypts each stored credential. The DEK itself is wrapped by a Key Encryption Key (KEK) held in Cloudflare Secrets — never in Postgres.
Decryption happens only in-process inside a single Edge Function or Worker invocation, at the moment a connector needs the secret. Plaintext credentials are never logged, cached to disk, returned to the browser, or written to a queue. The UI shows the last 4 characters and the scope — nothing more.
browser ──HTTPS──► Edge Function ──fetch KEK──► Cloudflare Secrets
│
├──unwrap DEK──► per-tenant AES-256-GCM key
│
├──decrypt──► plaintext (in-memory only)
│
└──call──► provider API (CF/Supabase/...)
▲
└─ plaintext dropped on function exitThe KEK is versioned and bound by Additional Authenticated Data (AAD) to the tenant + credential ID, so a stolen ciphertext can't be replayed against a different row. See ADR-0001 — KEK versioning & AAD and ADR-0002 — Service-role key boundary below.
Auth — mandatory 2FA + scoped step-up.
Every Flarewatch account enrolls TOTP on first login. AAL1 is password-only; AAL2 is a fresh TOTP within the last few minutes. AAL1 is enough to view dashboards. AAL2 is mandatory for: revealing or rotating a stored credential, running anything in the SQL console, and any provider write action.
Step-up is scope-bound: a credential reveal mints a single-use token tied to one specific credential ID and one specific action. The token can't be replayed to reveal a different secret, and it expires on use or in 5 minutes — whichever comes first.
MCP authentication — asymmetric, short-lived, revocable.
The MCP server (stdio + HTTP) accepts only short-lived JWTs signed with an asymmetric key (EdDSA). Tokens carry aud=mcp, a jti for individual revocation, and the tenant + site scope minted at login time. The signing private key never leaves the Edge runtime; the public key is published so third-party clients can verify.
Every MCP request rebinds the tenant context from the token claims before any RLS-bearing query — there is no ambient "current tenant" that could leak across a misrouted call. See ADR-0003 — MCP JWT design below.
Audit log — tamper-evident, force-RLS, 1-year retention.
Every credential decrypt, every reveal, every rotation, every SQL console statement, and every provider write writes one row to audit_log. Each row carries a hash of the previous row (chained), so any insertion, deletion, or reordering breaks the chain.
The table uses FORCE RLS — even Postgres superusers cannot bypass tenant isolation on read. A nightly Worker re-verifies the chain end-to-end and emits a signed receipt. Logs are retained for 1 year by default; Agency-tier accounts can export them on demand. See ADR-0004 — Tamper-evident audit chain below.
Public Decision Records (ADRs) — founder-signed commitments.
Published 2026-06-04. Each ADR is a specific, dated commitment. If the running code drifts from any of these, it's a bug — report it to security@flarewatch.dev.
KEK versioning & AAD
Every wrap carries a key version + tenant/credential AAD so a stolen ciphertext is non-portable and key rotation is invisible to callers.
Service-role key boundary
The Supabase service-role key never reaches a request-scoped Worker. Privileged work lives in a small set of explicitly enumerated Edge Functions.
MCP JWT design
EdDSA-signed, aud=mcp, jti-revocable, tenant rebind per request. Symmetric secrets are explicitly rejected.
Tamper-evident audit chain
Append-only audit_log with prev-row hash, FORCE RLS, nightly verification, 1-year retention.
What we'd do if breached.
Specific, dated commitments. The clock starts when we have a credible signal — not when we are sure.
- T+15mRevoke every minted MCP token and step-up token. Disable connectors for affected tenants. Triggered by a single command, exercised quarterly.
- T+4hEmail every affected tenant with what we know, what we don't, and which credentials we recommend rotating. Plain language, no PR-speak.
- T+7dPublish a public incident report. What happened, what was accessed, what wasn't, and the timeline of our response.
- T+30dPost-mortem RFC with concrete preventive actions, each linked to a tracked engineering ticket. Reviewed publicly.
Self-host — for the truly paranoid.
Self-host packaging ships in Phase 6. It includes the Next.js app, the Supabase schema + RLS policies, the Worker bundle, and the Edge Function bundle — deployable into the agency's own Cloudflare and Supabase accounts. There is no default KEK: first boot refuses to start until the operator generates one. Your credentials never touch our infrastructure.
Contact — responsible disclosure.
Report vulnerabilities to security@flarewatch.dev. A PGP key will be published with the Phase 0 launch. Until then, use the email address — we monitor it.
Our commitment to researchers: good-faith reports will never be met with legal threats. We will acknowledge within 72 hours, agree on a disclosure timeline, and credit you in the advisory unless you ask us not to.
This page is a public commitment. If you find anything here that doesn't match the running code, that is a bug — please report it to security@flarewatch.dev.