The JWT/JWKS route preserves RLS, but I would be careful about putting organization membership directly into a long-lived token. A signed org\_id claim proves what the issuer believed when the token was created. It does not prove the user is still a member when the query runs. Removing someone from an organization will not invalidate an already-issued token unless you have short expiries or an explicit revocation mechanism. For stronger tenant isolation, use the JWT subject as identity and keep current organization membership in a database table that the RLS policy joins against. Then removal takes effect immediately. If membership must remain in claims for performance, keep tokens short-lived and test the negative case: remove a member, replay their old token, and prove cross-tenant reads and writes fail.
One complication is consistency between the database dump and the Storage copy. They are two separate snapshots, so an object can be added, replaced or deleted between them. A successful dump plus a successful rclone run does not necessarily describe one recoverable point in time. I’d avoid rclone sync directly into the latest backup because source-side deletion can propagate into the backup. Write each run to a versioned prefix instead, then store a manifest containing every object key, size, ETag/checksum and backup timestamp. Enable object versioning or retention on the destination as well. The restore test should verify relationships, not just that both halves restore: every storage.objects row should resolve to the expected object version, and unexpected objects should be reported. That is where a backup assembled from two individually successful jobs can still fail during recovery.
Yes, but I’d have the app generate reviewed SQL rather than require owner credentials and create the role silently. The flow could be: calculate the minimum privileges, show the script and expected permission diff, let the user apply it through an admin channel, then reconnect with the diagnostic role and run negative capability tests. I’d set default\_transaction\_read\_only, add a statement timeout, revoke schema creation where it isn’t needed, and fail setup if the role has write privileges, BYPASSRLS, or access to unrelated SECURITY DEFINER functions. That keeps the privileged bootstrap step separate from normal analysis.
“Read-only” is necessary, but it would not be enough for me to connect a desktop analyzer to production. I’d want a dedicated Postgres role with no write privileges, no ownership, no `BYPASSRLS`, no function-execution privileges beyond an allowlist, and access restricted to the diagnostic views it actually needs. The semantic layer also changes the threat model. If schema metadata, query text, or samples leave the machine for an AI feature, the UI should show exactly what is transmitted and let users disable that path independently. One useful built-in check would be warning when the supplied role can see more than the analyzer requires. That turns least privilege into something the tool verifies instead of something the setup guide merely recommends.
That answer is useful because it confirms the requirement is the assessment outcome, not one specific document. I’d turn the alternatives into a small evidence matrix: risk being evaluated, artifact that addresses it, scope, reporting period, exceptions, owner, and next review date. The main trap is collecting ten documents that all describe the same control while leaving a gap elsewhere. I’d also record residual gaps explicitly—for example, an ISO certificate may establish the ISMS scope but not give the same control-testing detail as a Type II report. That makes the decision defensible even when the evidence package is mixed.
Before migrating, I'd ask the auditor or Vanta contact to name the exact vendor-control assertion they cannot support without the Type II report. "We need the report" is a document request; the underlying objective may be narrower. I'd assemble a documented vendor review using whatever evidence is available: security and architecture documentation, DPA and subprocessors, encryption and access-control descriptions, backup and recovery commitments, incident/status history, contractual terms, and a shared-responsibility mapping. Record the unavailable report as a limitation, then document the residual risk, owner, compensating controls, review date, and conditions that trigger an upgrade. There is also now a Supabase team response in this thread mentioning an early-stage startup workaround. I'd exhaust that route before either migrating or doing a temporary upgrade. Vendor monitoring is recurring, so confirm whether any supplied evidence remains accessible for future review rather than solving only the current collection step.
This is a good idea for a focused tool. One thing worth confirming it covers, since it's the gap that bites people even when every policy looks airtight: does it check whether FORCE ROW LEVEL SECURITY is set on each table? By default RLS doesn't apply to the table owner or to roles with BYPASSRLS, so a project can have well-written policies on every table and still leak data through any code path that runs as the owner (migration scripts, some server-side clients) unless FORCE is explicitly set. Same question for SECURITY DEFINER functions — a function defined with elevated privileges runs with the definer's permissions regardless of what RLS says, so an audit of "every table, view, function, and role" should really flag any SECURITY DEFINER function touching a protected table, not just check whether the table has RLS enabled. Those two are the ones that show up in real projects that otherwise look correctly configured.
The function-grants allowlist pattern maps cleanly onto RLS: pg\_policies gives you policy name, table, command, roles, and the qual/with\_check expressions in one queryable catalog, so "assert every table has policies matching a declared spec" is the same shape of test, just walking a different system table. One extra gotcha worth building into that sweep from day one: RLS being enabled doesn't bind the table owner or any role with BYPASSRLS — by default the owner of a table bypasses its own RLS policies unless you also run ALTER TABLE ... FORCE ROW LEVEL SECURITY. Easy to miss because your app's own queries (running as owner, or with the service role in a migration script) will look correctly scoped in testing even when a policy is silently not enforced for that role. Worth asserting FORCE ROW LEVEL SECURITY is set alongside "policy set matches intent" in the enumeration, not just RLS enabled.
The replies so far are answering the cost/CDN question, but the security half of "not to be shared" is the part to get right before worrying about egress bills. Two things specifically: make the storage bucket private, not public, and write an RLS policy on storage.objects so a user can only read/write objects under their own folder path — the default Supabase setup doesn't give you that automatically. Then serve images through short-lived signed URLs rather than public URLs. If you ask Claude Code to do this, ask it explicitly to write a private bucket with a folder-scoped RLS policy and a signed-URL helper — "add photo storage" on its own tends to reach for whatever's simplest to get working, which is usually a public bucket.
Worth flagging one more failure mode on the "where it stops working" list: once everything shares one Postgres instance, an RLS policy bug in any single app's schema is no longer contained to that app — a broken or missing WITH CHECK on one product's table doesn't stay a one-app problem the way a separate project would. Doesn't argue against consolidating, but it does mean the adversarial cross-tenant test (can org A read/write org B's row) needs to run against every schema, not just the one you're actively changing, since a migration in product 3 can't quietly weaken isolation for product 1.
The one that bites hardest in practice is 'checking reads but forgetting WITH CHECK on inserts/updates' — it's invisible in normal testing because your own writes look fine, and it only shows up when someone probes another tenant's insert path. Worth calling out for anyone vibe-coding this with AI: the assistant will happily generate a SELECT policy and just... not generate the write-side one, and nothing errors. If you're auditing an app after the fact, diffing every table's SELECT vs INSERT/UPDATE policy pairs is a five-minute check that catches this class of bug on its own.