Open the Supabase Cloud dashboard and you get two tabs self-hosters never see: Security Advisor and Performance Advisor. They flag tables with RLS disabled, views that leak auth.users, foreign keys with no index — the kind of problems that don't announce themselves until an incident does. Self-hosted Studio doesn't include them, and it's one of the gaps we called out in what features are missing in self-hosted Supabase.
Here's the part most self-hosters don't know: the Advisors are just SQL. Supabase publishes every lint as an open-source project called Splinter, and you can run the exact same checks against your own instance with psql. This guide covers how to run them, which findings actually matter, one caveat that silently hides results on self-hosted setups, and how to automate the whole thing.
What the Advisors Actually Are
When Supabase launched the Advisors, they built them as a set of Postgres queries rather than a proprietary scanner. Each lint inspects the system catalogs — pg_class, pg_policy, pg_index, and friends — and returns findings as rows. The dashboard is just a UI over the result set.
Each finding includes a name, a level (ERROR, WARN, or INFO), categories (SECURITY or PERFORMANCE), a human-readable detail, and a remediation URL pointing at Supabase's docs for that specific lint.
The lints that matter most in production:
| Lint | Level | What it catches |
|---|---|---|
rls_disabled_in_public | ERROR | Tables exposed via the Data API with no RLS at all |
policy_exists_rls_disabled | ERROR | Policies written, but RLS never enabled — policies do nothing |
auth_users_exposed | ERROR | Views or tables exposing auth.users to anon/authenticated |
security_definer_view | ERROR | Views that bypass the querying user's RLS |
function_search_path_mutable | WARN | Functions vulnerable to search-path hijacking |
auth_rls_initplan | WARN | Policies calling auth.uid() per-row instead of once per query |
unindexed_foreign_keys | INFO | FKs that force sequential scans on joins and cascades |
multiple_permissive_policies | WARN | Stacked permissive policies that each run on every query |
duplicate_index / unused_index | WARN/INFO | Write overhead with no read benefit |
If you've read our complete guide to Row Level Security, you'll recognize that half the security lints are RLS mistakes. That's not a coincidence — misconfigured RLS is the single most common way self-hosted Supabase projects leak data.
Running Splinter Against Your Instance
Splinter ships a single combined query, splinter.sql, containing every lint. Grab it and run it against your database. On a standard Docker Compose deployment:
# Download the latest combined lint query curl -sLO https://github.com/supabase/splinter/releases/latest/download/splinter.sql # Copy it into the db container and run it docker cp splinter.sql supabase-db:/tmp/splinter.sql docker exec -it supabase-db \ psql -U postgres -d postgres -f /tmp/splinter.sql
Or from any machine that can reach your database:
psql "postgresql://postgres:<password>@your-server:5432/postgres" \ -f splinter.sql
You'll get back one row per finding:
name | level | categories | detail
------------------------+-------+--------------+------------------------------------------
rls_disabled_in_public | ERROR | {SECURITY} | Table `public.invoices` is public, but
| | | RLS has not been enabled.
auth_rls_initplan | WARN | {PERFORMANCE}| Table `public.orders` has a RLS policy
| | | that re-evaluates auth.uid() for each row.
unindexed_foreign_keys | INFO | {PERFORMANCE}| Table `public.line_items` has a foreign
| | | key `order_id` without a covering index.
The Supabase CLI also has a db advisors command that wraps these lints, but it's aimed at local CLI-managed projects — for a production server, running splinter.sql directly is simpler and works everywhere psql does.
The Caveat That Hides Findings on Self-Hosted Setups
Several of the most important lints — auth_users_exposed, materialized_view_in_api, foreign_table_in_api — only report objects that are reachable through the Data API. They decide what's "reachable" by reading the pgrst.db_schemas setting, which exists inside PostgREST's connection but not in a plain psql session. Without it, these lints quietly fall back to checking public only.
If you expose additional schemas through PostgREST, mirror your real configuration before running the lints:
-- Match the PGRST_DB_SCHEMAS value from your .env
select set_config('pgrst.db_schemas', 'public,api,storage', false);
\i /tmp/splinter.sql
Check your compose file's PGRST_DB_SCHEMAS value — our environment variables guide covers where each of these settings lives. Getting this wrong doesn't produce an error; it produces a clean-looking report that skipped your API surface entirely, which is worse.
One more Data API note: with Supabase's October 2026 change to Data API grants, new deployments no longer hand broad default privileges to anon and authenticated. The advisors complement that change nicely — grants control whether a role can touch a table, RLS lints verify which rows it can see. You want both layers checked.
Triage: What to Fix First
Don't try to clear the whole report in one sitting. Work it in this order:
ERROR-level security findings, immediately.rls_disabled_in_publicandauth_users_exposedare live data exposure, not hygiene.policy_exists_rls_disabledis the sneakiest — someone wrote policies, assumed they were active, and never ranalter table ... enable row level security.security_definer_viewandfunction_search_path_mutable. These need judgment. Some SECURITY DEFINER views are intentional (that's how you build controlled cross-tenant reads), so verify rather than blindly "fix." For functions, pin the path:alter function my_fn() set search_path = '';auth_rls_initplan. The highest-leverage performance fix on this list. Wrappingauth.uid()in a subselect —using ((select auth.uid()) = user_id)— makes Postgres evaluate it once per query instead of once per row. On a 100k-row table, that's routinely a 10–100x difference.- Index findings. Add covering indexes for flagged foreign keys and drop true duplicates — our database indexing guide walks through the verification steps before you drop anything.
Automating the Advisors
A lint you run once is a snapshot; the value comes from running it on a schedule and catching regressions when a migration ships. Two patterns work well.
Scheduled run on the server, using plain cron (or pg_cron if you prefer keeping it in-database — see our cron jobs guide):
# /etc/cron.d/supabase-lint — every Monday 06:00 0 6 * * 1 deploy psql "$DB_URL" -f /opt/splinter/splinter.sql \ --csv -o /var/log/supabase-lint/$(date +\%F).csv
CI gate on migrations, failing the pipeline if any ERROR-level finding appears:
psql "$DB_URL" -f splinter.sql --csv -o results.csv if awk -F',' '$2 == "ERROR"' results.csv | grep -q .; then echo "Security advisor errors found:" && awk -F',' '$2 == "ERROR"' results.csv exit 1 fi
This is the same philosophy behind our production hardening guide: security posture on self-hosted infrastructure isn't a one-time checklist, it's a loop.
If you manage several projects, this compounds. Running lints across ten databases by hand doesn't happen in practice — script it against each project's connection string. Supascale's REST API exposes your project inventory, so a single script can enumerate every instance you run and lint them all in one pass; on the one-time license, that works across unlimited projects without per-project fees.
Honest Limitations
The advisors are useful, not sufficient. Know what they don't do:
- They're point-in-time schema checks. Splinter reads catalogs, not traffic. It will never tell you a query got slow last Tuesday — that's a job for real observability and
pg_stat_statements. unused_indexlies on young databases. Index usage stats accumulate over time and reset onpg_stat_reset(). An index flagged as unused two days after deployment isn't unused — it's new.- Some findings are deliberate. SECURITY DEFINER views and extensions in
publicare sometimes exactly what you meant. TreatWARNandINFOas questions, not verdicts. - Cloud gets the UI; you get the data. There's no pretty dashboard for these results self-hosted. A CSV in CI and a failing pipeline is less polished — and arguably more effective, because it blocks the regression instead of displaying it.
Wrapping Up
The Advisors gap in self-hosted Supabase is smaller than it looks: the entire lint suite is open source, runs anywhere psql runs, and takes about ten minutes to wire into cron or CI. Set pgrst.db_schemas to match your real PostgREST config so the API-facing lints see what your API sees, clear the ERROR-level security findings first, fix auth_rls_initplan for the cheapest performance win you'll get this quarter, and make the run automatic so schema regressions get caught before your users do.
Further Reading
- Row Level Security for Self-Hosted Supabase: A Complete Guide
- Supabase October 2026 Breaking Change: Data API Grants Explained
- Securing Self-Hosted Supabase: A Production Hardening Guide
- Database Indexing for Self-Hosted Supabase: A Complete Guide
- Self-Hosted Supabase Environment Variables: A Complete Guide
