Every row is owned by your tenant.
Tenancy
Every table has an `org_id`. Every query goes through `scope(orgId)` which appends a `WHERE org_id = $N` filter at the SQL layer — the raw Drizzle client isn't even exported from the package root. A unit test in `@rlr/db` asserts every query method's SQL contains that filter. The four exceptions (creating a new tenant, the daily cron fan-out, the weekly digest fan-out, the audit-org GC) are documented in ARCHITECTURE.md §3.
verified by test
Read-only behavior, enforced by code.
Access
Our audit funnel calls only Stripe's read APIs — listing charges, invoices, subscriptions. The write side is gated by the safety backstop: every recovery action goes through `withSafetyBackstop`, which only fires after you explicitly toggle that specific action on, with per-action dollar ceilings and rate limits applied in code. (Note: we requested Stripe's `read_only` OAuth scope but it's gated behind their support approval for new platforms; pending approval, the granted scope is `read_write`. The backstop is the real enforcement either way — see recovery/backstop.ts.)
code-enforced
Encrypted at rest with per-record AES-256-GCM.
Tokens
OAuth tokens for the providers we store them for (Xero, QuickBooks) are encrypted before they leave the `scope.integrations.upsertWithTokens()` boundary. A scope test asserts plaintext does not appear in the generated SQL parameters. Stripe uses our platform key + the `Stripe-Account` header — we never receive a Stripe customer's access token in the first place.
verified by test
Two-axis backstop on every action.
Recovery safety
Every recovery action attempt — manual or auto — routes through `withSafetyBackstop`. It enforces a per-action dollar ceiling ($500 by default; you can raise it on the scope-settings page) and, for customer-facing actions only, a rate window (5/hour, 20/day). The wrapper writes an `action_events` row for every attempt — drafted, executed, failed, or rejected_by_backstop — so the audit log is exhaustive by construction.
ceiling + rate cap
We don't claim recovery until the provider confirms it.
Attribution
An hourly attribution probe re-fetches the post-state from the provider (e.g. an invoice retry: is `status === 'paid'`?) and writes the verdict into `action_events.resolved_state`. The dashboard's "Recovered to date" KPI sums only confirmed amounts — never optimistic projections.
verified by provider
Per-org caps so a runaway scan can't fan out.
Concurrency
The Inngest function that fires auto-mode recovery actions is gated by `concurrency: {limit: 3, key: orgId}`. Even a misconfigured detector flagging a hundred findings can only act on three of your invoices in parallel — capped by both this and the backstop's rate window.
limit=3 per org