Internal tools pointed straight at PostgREST
Internal tools acquire Supabase backends the convenient way: point Retool, Bubble or a similar builder straight at the PostgREST endpoint, paste the URL and a key, bind resources to tables, done. The dashboard works by lunchtime. What the convenience quietly arranges is an identity mismatch — every user of the internal tool is authenticated inside the tool and anonymous inside Supabase — plus a fact about networks that surprises people regularly: the PostgREST endpoint is on the public internet, whatever word like "internal" evokes.
Those two forces produce a characteristic failure. Policies keyed to identities the tool never presents return nothing; the friction gets resolved by widening access; eventually a table ships readable by anyone holding the public key, protected chiefly by the belief that nobody outside the company knows it exists. This page covers the mechanics of that failure, the one request that tests for it, and the two architectures that resolve the identity mismatch honestly.
Rules that check this
- criticalTable readable with the anon key
ANON_TABLE_READABLE
Direct connections, inverted identities
The builder authenticates its operators richly — single sign-on, roles, audit trails. Supabase sees none of it. Requests arriving at PostgREST carry whichever Supabase key the integration was configured with; configured with the anon key, every request is an unauthenticated request, and policies that filter on auth.uid() evaluate against an empty identity, returning nothing. The tool displays empty tables; somebody adjusts the policy or removes RLS until data appears.
Notice what the adjustment optimises for: making the internal tool work, today, for the team waiting on it. Everyone in the loop behaves reasonably. The widening persists because nothing ever fails again — reads succeed indefinitely for every audience, internal and otherwise alike. The failure is not a decision anyone made; it is a decision nobody had to make.
The endpoint is public even when the tool is not
PostgREST endpoints accept connections from anywhere; there is no IP allowlist by default and no corporate perimeter in front of them. Possession of the URL and anon key — both public artifacts — is the only requirement for probing. The enumeration request below lists every relation the exposed schema offers, followed by the read test for any table the list reveals:
Neither request requires anything secret: the URL identifies the project publicly and the anon key rides in every bundle the tool or frontend ever served. That is the design working as drawn. The test is worth running precisely because it costs nothing, touches nothing, and reports the world exactly as a curious stranger would find it.
curl -s "$SUPABASE_URL/rest/v1/" \ -H "apikey: $ANON_KEY" | head -c 500 curl -s "$SUPABASE_URL/rest/v1/customers?select=name,email&limit=5" \ -H "apikey: $ANON_KEY" \ -H "Authorization: Bearer $ANON_KEY"
Reading the verdict
Rows returned mean customer records — or whatever the table holds — are readable worldwide, and the finding stands regardless of how internal the tool feels. A permission error means policies engaged against the anonymous caller, which is the healthy outcome. The empty array is the ambiguous middle: filtered or empty, indistinguishable from outside, deserving the honest label reachable-unverified rather than clean. Internal tools earn the same verdicts as public apps precisely because the endpoint cannot tell the audiences apart.
Write paths deserve a specific caution for this integration style: teams sometimes paste the service_role key into the tool to eliminate permission friction wholesale. That relocates the bypass credential into another vendor's infrastructure — better than shipping it to browsers, materially worse than a scoped database role, and it silently voids every policy in the project for that connection. Least privilege survives translation: connect integrations with the narrowest credential that works.
Two architectures that resolve the mismatch
The honest options both supply Supabase a real identity. Issue genuine Supabase sessions to tool users — provision accounts, sign them in through the tool, and let existing auth.uid()-scoped policies work unchanged. Or interpose a thin server the tool calls instead: it holds a privileged credential server-side, enforces tool-side authorisation, and presents narrowly-scoped queries downstream. Both bound the anonymous surface to nothing; they differ in where the complexity lands.
Whichever route, the anonymous surface that remains — legacy tables, migration leftovers, anything the tool stopped using — still warrants the standing test. RowShield runs it continuously: the probe enumerates and reads as the anon key, reporting exposed tables by column name and count with GET requests only, while connected scans audit RLS state and policy sanity in the catalog. Alerts fire on transitions — created, regressed, resolved — so an internal-tool shortcut that leaks raises an event rather than a weekly reminder. RowShield is an independent product, unaffiliated with and not endorsed by Supabase, reading pg_catalog and bucket metadata only, never rows.
Frequently asked
- Is connecting Retool or Bubble with the anon key wrong by itself?
- It is workable only while every touched table has policies that deliberately serve anonymous callers — rare for internal data. The common outcome is accidental widening instead. Prefer issuing real Supabase identities to tool users, or a server intermediary holding a scoped credential, so policies evaluate against actual principals.
- Nobody knows my project URL. Is that not protection?
- URLs travel — through bundles, support tickets, screenshots and logs — and enumeration costs one request once someone holds the pair. Secrecy of the endpoint is not an access-control layer; policies enforced at the database are. Testing with the anon key shows exactly what a discoverer would find on the first try.
- Our tool uses the service_role key instead. Are we covered?
- Covered against lost functionality, exposed to worse: that key bypasses every policy, and it now sits in a third-party platform as a single credential for everything it can reach. Safer than the same key in a browser, weaker than a dedicated scoped role — and the catalog audit, not the tool configuration, becomes the only honest map of exposure.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit