Harden any Supabase project with one command. RLS-by-default, append-only audit log with hash chain, column-level encryption, password hardening, and a built-in security advisor.
DATABASE_URL='postgres://postgres:...@db.<ref>.supabase.co:5432/postgres' \
bash <(curl -fsSL https://raw.githubusercontent.com/mchawda/supabase_secure/main/install.sh)That's it. No clone, no Node, no Docker. Just psql.
Don't have DATABASE_URL handy? Run it without and the installer will
prompt you for your project ref + DB password.
git clone https://github.com/mchawda/supabase_secure
cd supabase_secure
DATABASE_URL='postgres://...' ./scripts/apply.sh
./scripts/verify.shsupabase link --project-ref <your-ref>
cp -r path/to/supabase_secure/supabase/migrations/* supabase/migrations/
supabase db pushAfter you run the installer, your Supabase database has:
| Feature | Detail | |
|---|---|---|
| 🔒 | RLS forced on every public table |
Existing tables hardened; an event trigger turns RLS on automatically for any new table you create. |
| 📜 | Append-only audit log with SHA-256 hash chain | Every INSERT/UPDATE/DELETE on public.* is captured into security.audit_log. Tamper-evident: select * from security.audit_verify_chain(null) returns 0 rows when the chain is intact. |
| 🔑 | Column-level encryption via Supabase Vault | security.encrypt_text() / security.decrypt_text() wrap Vault. Plaintext never touches your tables. |
| 👤 | Password hardening | Auth Hook rejects passwords under 12 chars, no upper/lower/digit/symbol, or in the common-password list. |
| 🚦 | Brute-force throttle | security.is_login_throttled() for your sign-in API. |
| 🪪 | Multi-tenant RBAC helpers | security.current_org_id(), security.is_admin(), security.has_role(), security.require_mfa() — all read from the safe app_metadata JWT claim. |
| 🗂️ | Storage RLS | Default-deny baseline + security.add_org_scoped_bucket_policies('bucket') to wire a tenant in one line. |
| 🩺 | Built-in advisor | select * from security.advisors shows tables without RLS, views without security_invoker, SECURITY DEFINER leaks in public, broken audit chain, and more. |
| 🔐 | Lockdown defaults | Revokes ambient PUBLIC grants, hides private schemas, pins search_path, blocks CREATE in public for anon/authenticated. |
All of this is idempotent — re-run the installer any time.
-- One row per misconfiguration. Should be empty (or only false-positives you accept).
select * from security.advisors order by severity;
-- Chain integrity (must return 0).
select count(*) from security.audit_verify_chain(null);Or run the 10 self-tests:
psql "$DATABASE_URL" -f tests/security_tests.sql
# => OK: 10 tests passedThese are settings the SQL kit cannot change for you, but the installer will remind you:
- Activate the password hook in Supabase Studio →
Authentication → Hooks → Password validation:
pg-functions://postgres/security/password_validation_hook - Enable MFA at the Supabase organisation level.
- Custom SMTP + CAPTCHA on sign-in / sign-up / reset.
- Network restrictions in Database → Settings (IP allow-list).
- Put
org_id uuid not nullon every tenant table; the canonical pattern is inexamples/tenant_table_template.sql.
examples/tenant_table_template.sql— multi-tenant table + the four canonical RLS policies.examples/encrypted_column_example.sql— Vault-backed sensitive column with an audited read RPC.
supabase/migrations/
20260429000100_security_extensions.sql ← pgcrypto, vault, pg_stat_statements
20260429000200_security_schema_and_helpers.sql ← private security schema + JWT helpers
20260429000300_security_audit_log.sql ← append-only audit log + hash chain
20260429000400_security_audit_triggers.sql ← generic trigger + auto-attach
20260429000500_security_encryption.sql ← Vault-backed encryption helpers
20260429000600_security_rls_defaults.sql ← force RLS + auto-RLS event trigger
20260429000700_security_storage_policies.sql ← storage default-deny + builders
20260429000800_security_auth_hardening.sql ← password hook, throttle, sessions
20260429000900_security_lockdown.sql ← revoke PUBLIC grants, pin search_path
20260429001000_security_advisors.sql ← security.advisors view
scripts/
apply.sh ← apply via psql or supabase CLI
verify.sh ← print advisors + chain status
rollback.sql ← clean removal (preserves Vault secrets)
install.sh ← one-line installer (download + apply + verify)
tests/security_tests.sql
examples/
psql "$DATABASE_URL" -f scripts/rollback.sqlRemoves the security schema, the audit triggers, and the event
triggers. Does not drop supabase_vault — your encrypted secrets
stay intact.
Will this break my existing app?
It enables RLS on tables that didn't have it. If you currently rely on
unrestricted access from anon / authenticated, you'll need to add
explicit policies. Use select * from security.advisors where kind = 'rls_no_policies'
to find such tables. Apply on staging first.
Why not pgsodium for column encryption? Supabase has deprecated pgsodium TCE. Vault is the supported replacement and keeps key material outside the database.
Why is user_metadata not used in any helper?
Because raw_user_meta_data is user-editable and unsafe for
authorization. Always use app_metadata (set server-side with the
service role).
Does this work with the Supabase free tier? Yes. All extensions used are available on every tier.
MIT. See LICENSE.