Guides / Reliability

Supabase already ships the check that finds an open table. It only helps if something runs it.

On 25 September 2026 UpGuard published the largest study yet of exposed Supabase databases: 16,326 databases exposing readable tables, found by probing about 300,000 domains for a table called users. Supabase already ships a check that flags this exact failure, and rates it an error: Anyone with your project URL can read, edit, and delete all data in this table. The check lives in a dashboard tab. An app built by an agent often has nobody who opens that tab after launch, and the check cannot tell anyone on its own. This guide covers what the Security Advisor and the Health Check Advisors that shipped on 18 September actually detect, how to run them on a schedule so the findings come to you, the checks they do not do, and how to let an agent watch the project without handing it the keys. Every Supabase behaviour below comes from Supabase's own documentation, changelog and API specification, read on 28 September 2026.

Sixteen thousand open databases, and a check that already existed.

UpGuard's report, Everything Everywhere: Systemic Data Exposure in Supabase Apps, published on 25 September 2026 and reported by TechCrunch the same day, started from the web rather than from a vibe-coding platform. It collected about 300,000 domains with signs of Supabase use, then asked each database for a table called users. The answer came back in one of three forms: nothing accessible, a page of rows, or no users table but some data accessible, in which case "The database would then provide the name of an accessible table as a 'hint.'" The count: "we identified 16,326 databases exposing readable tables." Over half showed indicators of personal information; a smaller share held passwords or authentication tokens. The examples UpGuard verified include a valet service's CRM with over 100,000 customers and a relocation service with 884 plain-text passwords.

The mechanism is the one the RLS guide is built around, and UpGuard names why it keeps happening in agent-built apps: Supabase enables Row Level Security by default for tables created in the Table Editor, but "Tables created programmatically through the API, which is how coding agents interact with Supabase, do not enable RLS by default." An agent that writes create table public.orders (...) in a migration and never writes alter table ... enable row level security has produced a table that anyone with the project URL can read. Supabase's security chief told TechCrunch that projects are "secure by default" and described security as shared with customers. Both statements can be true at once, and that is the problem: the default protects the dashboard path, and agents do not use the dashboard path.

Here is the part that makes this a monitoring guide. Supabase's Security Advisor has a check for exactly this failure, 0013_rls_disabled_in_public, level ERROR, and its ramification line reads: "Anyone with your project URL can read, edit, and delete all data in this table because Row-Level Security is not enabled." Every access path Supabase documents for it (Studio, the MCP server, the CLI, the Management API) is one you open or call. In the documentation we read there is no description of an advisor finding being sent to you. A check that waits to be looked at is only as good as the schedule of whoever looks.

What the advisors actually check.

Supabase describes Advisors as "programmatic checks that ship with the platform. They inspect the live schema and return deterministic findings." There are two original families, Security and Performance, and a third, Health, added on 18 September 2026. Each finding carries a check name, a level (ERROR, WARN or INFO), the affected object and remediation text. The security checks that matter most for an AI-built app, in Supabase's own words:

  • 0013_rls_disabled_in_public, ERROR. A table in public without RLS: "anyone with the project's URL can CREATE/READ/UPDATE/DELETE (CRUD) rows in the impacted table." This is the UpGuard failure.
  • 0023_sensitive_columns_exposed, ERROR. The same condition, on a table whose column names match patterns such as password, token, api_key, passport_number, national_id, bank_account or card_number.
  • 0024_permissive_rls_policy, WARN. RLS is on, but a policy says USING (true), USING (1=1) or WITH CHECK (true), or a permissive SELECT policy has no USING clause at all. Supabase lists "placeholder policies during development" among the causes, which is the agent pattern: a policy written to make an error go away.
  • 0007_policy_exists_rls_disabled, INFO. Policies were written but RLS was never enabled, so they do nothing. Note the level: INFO, although the table is as open as one caught by 0013.
  • 0025_public_bucket_allows_listing, WARN. A public Storage bucket with a broad SELECT policy on storage.objects, so anyone can list every file in it, not only fetch the ones they know about.
  • 0008_rls_enabled_no_policy, INFO. RLS on, no policies, so the Data API returns nothing. Usually harmless; Supabase suggests an explicit using (false) policy if the lockout is deliberate.

The Health family is newer and narrower. Version 1 is four error-rate checks read from log data: log_data_api_error_rate_high (PostgREST 5xx), log_auth_error_rate_high, log_storage_error_rate_high and log_edge_function_error_rate_high. They appear under a Health tab above Security in Studio's Advisors section, and through POST /v2/projects/{ref}/advisors/run in the Management API, which returns health results alongside security and performance and caches them. One design detail is worth copying into your own monitoring: "An empty result means all checks ran and found nothing. advisor_check_unavailable means a check could not run - so you can distinguish between healthy and unchecked." Most homemade monitors cannot tell those two apart. Supabase's changelog says database connectivity probes and instance health checks are next on the roadmap, not shipped.

Make the findings come to you: run the advisors on a schedule.

The Management API exposes the same checks. GET /v1/projects/{ref}/advisors/security returns an object with a lints array, and each lint has a name, a level, categories, a detail and a remediation. Supabase marks the endpoint experimental ("subject to change or removal"), and it needs the database:read OAuth scope. The same API reports project state (GET /v1/projects/{ref}, where the status field is ACTIVE_HEALTHY when all is well and INACTIVE, PAUSING, RESTORING or one of eleven other values when it is not) and per-service health (GET /v1/projects/{ref}/health). Put the three together in one script that exits non-zero when a human is needed, and run it from a scheduler you control.

#!/bin/sh
# advisors-check.sh - exit 1 if the project is not healthy or the
# Security Advisor reports an ERROR or WARN finding not on the allowlist.
# SUPABASE_ACCESS_TOKEN is a Management API token for your account:
# keep it in the scheduler's secret store, never in the repo or frontend.
REF="your-project-ref"
API="https://api.supabase.com/v1/projects/$REF"
AUTH="Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
fail=0

# 1. project state: paused, restoring, resizing all need a human
status=$(curl -sf -H "$AUTH" "$API" | jq -r '.status')
[ "$status" = "ACTIVE_HEALTHY" ] || { echo "PROJECT: $status"; fail=1; }

# 2. services: anything not ACTIVE_HEALTHY
health=$(curl -sf -H "$AUTH" "$API/health?services=auth,db,rest,storage") \
  || { echo "HEALTH: call failed, status unknown"; fail=1; }
echo "$health" \
  | jq -r '.[] | select(.status != "ACTIVE_HEALTHY") | "SERVICE: " + .name' \
  | grep . && fail=1

# 3. security advisor: ERROR and WARN, minus reviewed exceptions
curl -sf -H "$AUTH" "$API/advisors/security" -o security.json \
  || { echo "ADVISOR: call failed, status unknown"; exit 1; }
jq -r '.lints[] | select(.level != "INFO")
  | [.level, .name, (.metadata.schema // ""), (.metadata.name // "")]
  | join(" ")' security.json \
  | grep -v -x -F -f allowlist.txt > findings.txt
[ -s findings.txt ] && { cat findings.txt; fail=1; }

exit $fail

Three choices in that script are deliberate. A failed API call is a failure, not a pass: if the advisor cannot be reached, the script says so and exits, which is the same healthy-versus-unchecked distinction Supabase built into the Health checks. allowlist.txt holds findings you have reviewed and accepted, one line each, exactly as the script prints them, so a known exception does not page anyone every day while a new one does; create it empty on day one. And it prints check names and object names only, never rows, so the log of a monitoring job never becomes a copy of the data it is protecting.

Run it daily from a scheduled CI job, and after every migration that reaches production, because an agent's new table is the moment 0013 goes from clean to ERROR. For the Health checks, call POST https://api.supabase.com/v2/projects/{ref}/advisors/run with the same bearer token. The changelog shows the call but not the response shape, so start with the one thing you can match without parsing: alert if the body contains _error_rate_high or advisor_check_unavailable, and refine once you have seen real output from your own project. The Management API token acts on your account, not on one table, so treat it like the secret key: a scheduler secret, never a chat message to an agent.

A check you can run without any API The advisor is Supabase reading your catalogue. You can read it yourself, in the SQL editor or in CI against a branch, and get the 0013 answer from Postgres directly: select n.nspname, c.relname from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname = 'public' and c.relkind in ('r', 'p') and not c.relrowsecurity; Any row it returns is a table the Data API can reach with RLS off. For 0024 in its simplest form, select tablename, policyname, cmd, qual, with_check from pg_policies where schemaname in ('public', 'storage') and (qual = 'true' or with_check = 'true');

What the advisors do not see.

Policies that are wrong without being always-true. The 0024 check looks for the patterns it lists: true, 1=1, 'a'='a', a missing clause. A policy such as using (auth.uid() is not null) is not on that list, and it gives every signed-in user every row, which on an app with open sign-up is almost the same as no policy. So is a policy that compares auth.uid() to the wrong column. No lint can know which column means "owner" in your schema. The only test for that is the one the RLS guide describes: two test users, and user A trying to read user B's rows. The test data guide covers creating those users safely on a production database.

What an outsider actually gets. The advisors read your schema; UpGuard read your API. Run the outsider's version against your own project with only the publishable key your frontend already ships, one request per table, and print the verdict, never the body:

#!/bin/sh
# outside-in.sh - what an anonymous visitor can read, table by table.
# tables.txt: one public table name per line, from pg_tables.
URL="https://your-project-ref.supabase.co"
KEY="$SUPABASE_PUBLISHABLE_KEY"
while read -r t; do
  body=$(curl -s -H "apikey: $KEY" "$URL/rest/v1/$t?select=*&limit=1")
  case "$body" in
    "[]") echo "empty  $t" ;;
    "["*) echo "OPEN   $t   anonymous visitors can read rows" ;;
    *)    echo "denied $t" ;;
  esac
done < tables.txt

Read "empty" carefully. An RLS-protected table returns an empty list, but so does an open table that has no rows yet, so this check is only meaningful once real data exists. The SQL check above reads the setting, not the data, and has no such blind spot; run both.

Account state, before the Health checks grow up. Error-rate checks need traffic that errors. A paused project, an expired card, an overdue invoice or a free database in read-only mode can produce a quiet app, not a noisy one. The project status call in the script above catches a paused project from the control plane, and the heartbeat in the paused-app guide, which resolves the host, reads a row and writes a row, catches read-only mode and a hostname that no longer resolves. Keep both until Supabase ships the connectivity and instance checks it has announced.

Changes Supabase makes under you. The changelog is a monitoring input too. On 25 September 2026 Supabase announced the PostgreSQL 15.19 / 17.11 minor release, which closes 44 CVEs and is available for existing projects in the dashboard from 28 September. Two of its effects are silent: after the upgrade, searches on affected ltree indexes, and on btree_gist indexes over float columns containing NaN, "may silently return wrong or incomplete results - no error is raised" until the index is rebuilt, and PGP data encrypted with pgcrypto's bf, blowfish or cast5 ciphers "was effectively stored unencrypted". The entry ships the detection queries. No advisor, error rate or uptime monitor would have told you; someone reading the changelog would. Subscribe to it and read it weekly.

What monitoring costs. On the same day Supabase announced usage-based pricing for logs: ingest is metered, and log query, "the volume scanned when reading logs", gets a quota that scales with ingest. It is a soft launch, with usage visible in the project's usage dashboard, limits not yet enforced, a grace period through early 2027, and Supabase's statement that more than 90% of projects are within the included limits. A monitoring agent that scans every log hourly is exactly the workload that reads a lot. Check the meter after its first week, and see the bills guide for why the first invoice is the wrong place to find out.

Letting an agent watch the project, without giving it the keys.

Supabase now invites this directly. Its 13 September 2026 changelog entry, Observability on Auto-Pilot, adds a "Hire an agent" guide with four prepared roles: Health (API and Auth error rates, connection pressure, instance state), Security (Security Advisor findings, RLS gaps, Auth 4xx patterns), Performance (slow queries, lock waits, the Performance Advisor) and Capacity (growth against baselines), each ready for Claude Routines, Codex scheduled tasks or Cursor Automations. The Supabase MCP server gained a query_logs tool for log access across API, Auth, Storage, Edge Functions and Postgres, beside the existing get_advisors. It is a sensible idea with one rule attached, and Supabase states the rule itself: "An unattended monitoring routine cannot request approval during each run. Approve in advance only the project-scoped, read-only tools that the routine needs. The routine must stop and report a recommendation instead of running a write operation."

In practice that means three URL parameters on the MCP server, all documented: project_ref=<id> to scope it to one project (which also disables the account tools, including pause_project), read_only=true to "Execute all queries as a read-only Postgres user", and features= to enable only the tool groups the role needs.

https://mcp.supabase.com/mcp?project_ref=your-project-ref&read_only=true&features=debugging,docs

The Debugging group holds query_logs and get_advisors, which is everything a Health or Security role reads. Leave out the Database group unless the role must run SQL, because execute_sql and apply_migration live there. Supabase's own documentation carries the reason in a worked example: a support ticket whose text tells the agent to select from a sensitive table and post the result as a reply. A monitoring agent reads logs, and logs contain whatever your users typed. Read-only and scoped is what makes an injected instruction a failed query rather than a leak. The agent guardrails guide covers the wider pattern.

The monitoring an AI-built Supabase app actually needs.

  1. Today, by hand. Open Advisors, then Security, and fix every ERROR. Then look at every 0007 INFO finding, because a table with policies and RLS off is as open as an ERROR. Run the SQL check and the outside-in script once.
  2. This week, on a schedule. Put advisors-check.sh, or your own version of it, in a daily CI job with the token as a secret and an empty allowlist. Alerts go to an address two people read.
  3. On every migration. Run the SQL check against a branch or local database in CI before the migration reaches production, and fail the build on any row. This is the check that stops the agent's next table from shipping open.
  4. Keep the heartbeat. The error-rate checks do not see a quiet, paused or read-only project. The project status call and the paused-app heartbeat do.
  5. If an agent watches, scope it. One project, read-only, only the tool groups it needs, no approval-free write tools, findings reported rather than fixed.
  6. Read the changelog weekly. Silent changes, such as an index that returns wrong results after a minor upgrade, reach you by reading and by nothing else. The load guide and the backups guide cover the other two failures a monitor exists to catch early.

None of this is sophisticated. That is the point of the UpGuard number: sixteen thousand databases were not open because the problem is hard to detect. They were open because the detector was a tab nobody opened.

Questions

Questions founders ask.

What did UpGuard's September 2026 study of Supabase databases find?

UpGuard's report, Everything Everywhere: Systemic Data Exposure in Supabase Apps, published on 25 September 2026, collected about 300,000 domains showing signs of Supabase use and queried each database for a table called users. It identified 16,326 databases exposing readable tables. Over half had indicators of personal information, a smaller percentage included passwords or authentication tokens, and a very small number had plausible credit card data. Verified examples include a valet service CRM exposing over 100,000 customers and a Canadian relocation service with 884 plain-text passwords. UpGuard attributes the pattern to how coding agents create tables: Supabase enables Row Level Security by default for tables created in the Table Editor, but in UpGuard's words tables created programmatically through the API, which is how coding agents interact with Supabase, do not enable RLS by default. Supabase told TechCrunch its projects are secure by default and that security is a shared responsibility with customers.

Does Supabase warn me if a table has Row Level Security turned off?

It detects it. The Security Advisor check 0013_rls_disabled_in_public reports any table in the public schema without RLS at level ERROR, with the ramification that anyone with your project URL can read, edit, and delete all data in the table. A related check, 0023_sensitive_columns_exposed, raises an ERROR when such a table has column names that look like passwords, tokens, identity numbers or card numbers, and 0007_policy_exists_rls_disabled flags tables with policies but RLS off, at level INFO even though the table is equally open. The findings appear in Studio under Advisors, and through the MCP get_advisors tool, the supabase db advisors CLI command and the Management API. Every one of those paths is something you open or call; the Supabase documentation we read on 28 September 2026 does not describe advisor findings being sent to you. To be warned rather than to be able to look, run the check on a schedule and alert on the result.

What are Supabase Health Check Advisors?

A third family of Advisors, added on 18 September 2026, that monitors service health rather than schema. Version 1 has four error-rate checks read from log data: log_data_api_error_rate_high for PostgREST 5xx responses, log_auth_error_rate_high, log_storage_error_rate_high and log_edge_function_error_rate_high. They appear in a Health tab above Security in the Advisors section of Studio, with each result linking to the relevant logs or infrastructure view, and through POST /v2/projects/{ref}/advisors/run in the Management API, which returns health results alongside security and performance advisors and caches them. An empty result means all checks ran and found nothing, while advisor_check_unavailable means a check could not run, so you can tell healthy from unchecked. Supabase says database connectivity probes and instance health checks are next on its roadmap. Because they are error-rate checks, they need traffic that fails; a paused or quiet project produces no error rate.

How do I run Supabase advisors automatically, on a schedule or in CI?

Call the Management API from a scheduled job. GET /v1/projects/{ref}/advisors/security returns a lints array in which each finding has a name, a level of ERROR, WARN or INFO, categories, a detail and a remediation; Supabase marks the endpoint experimental and it needs the database:read OAuth scope. Filter out INFO, remove findings you have reviewed and listed in an allowlist, and exit non-zero if anything remains. In the same job, check GET /v1/projects/{ref} and alert unless status is ACTIVE_HEALTHY, since paused, pausing, restoring and resizing are all other values, and check GET /v1/projects/{ref}/health for the auth, db, rest and storage services. Treat a failed API call as a failure, not a pass, print check and object names rather than rows, and keep the access token in the scheduler's secret store. On every migration, also run a SQL check in CI against a branch or local database: select tables in the public schema from pg_class where relrowsecurity is false, and fail the build on any row.

What do the Supabase advisors not catch?

Four things matter most. First, policies that are wrong without being always-true: the permissive-policy check 0024 looks for patterns such as USING (true), 1=1 or a missing clause, so a policy like using (auth.uid() is not null), which gives every signed-in user every row, or a policy that compares auth.uid() to the wrong column, passes; only a two-user test proves isolation. Second, the outsider's view: advisors read your schema, so test your own API with the publishable key, table by table, the way UpGuard did. Third, account state: error-rate checks need failing traffic, so a paused project, an overdue invoice or a free database in read-only mode can stay quiet; a project status check and a heartbeat that performs a real write cover those. Fourth, platform changes: Supabase's PostgreSQL 15.19 / 17.11 release, announced on 25 September 2026, means affected ltree and btree_gist indexes can return wrong or incomplete results without an error until reindexed, and pgcrypto data encrypted with the bf, blowfish or cast5 ciphers was effectively stored unencrypted. Only reading the changelog tells you that.

Is it safe to let an AI agent monitor my Supabase project?

It can be, if it cannot write. Supabase's Observability on Auto-Pilot guide of 13 September 2026 offers prepared Health, Security, Performance and Capacity roles for scheduled agents, and its MCP server has query_logs and get_advisors tools for exactly this. Supabase's own rule is that an unattended monitoring routine cannot request approval during each run, so you approve in advance only the project-scoped, read-only tools it needs, and the routine must stop and report a recommendation instead of running a write operation. Configure the MCP server with project_ref to scope it to one project, which also disables account tools such as pause_project, read_only=true so queries run as a read-only Postgres user, and features=debugging,docs so SQL execution and migrations are not available. Supabase documents the reason with a prompt-injection example in which a support ticket instructs the agent to read a sensitive table. A monitoring agent reads logs that contain whatever users typed, so scoping and read-only mode are what turn an injected instruction into a failed query.

Before the next scan

Would you know tonight if your agent shipped an open table this afternoon?

A production-readiness review answers that with evidence: every advisor finding on your project read and triaged, the policies tested with two real users, your API probed the way an outsider would probe it, and monitoring that reaches a person when something changes. You get a ranked list of what to fix before real users, what can wait, and what is already fine - from the senior engineers who would do the work.