A small consultancy built an internal time tracking application. React frontend, Supabase backend, hosted on Cloudflare Workers. Built quickly, using modern tooling, to solve a real problem. Before opening it up to the wider team, they asked for a security review.

That instinct, to check before publishing  is the right one. Most people don’t.

Here’s what the review found.

The Application

The stack was straightforward: a single-page React app served as a static file on Cloudflare Workers, connecting directly to a Supabase project over HTTPS. Supabase handles the database (Postgres) and exposes an auto-generated REST API that the frontend queries directly. No server middleware. Okta was available as an identity provider but hadn’t been connected to this application yet.

The data in the database included client names, engagement details, billing rates, unbilled revenue, and pipeline information.

What Was Found

Finding 1: The Wrong API Key Was in the Frontend Code

Supabase provides two keys, a publishable key for client-side use, and a service role key that carries full database access. The service role key was embedded in the JavaScript delivered to every browser. Anyone who viewed the page source had it.

From there, they could query the database directly using nothing more than a terminal. No login required.

Finding 2: There Was No Authentication Gate

Navigating to the application URL loaded the dashboard immediately, with no login prompt. The data was accessible to anyone with the link and no session check, no redirect, no challenge of any kind.

Okta is available as a custom identity provider and needs to be configured as an OAuth/OIDC provider. It simply hadn’t been connected to this application yet.

Finding 3: Row Level Security Policies Were Open

RLS was enabled on all tables. The Supabase dashboard showed a green shield icon next to each one. The team had checked the box.

But the policy condition on each table was set to TRUE, which means every request passes regardless of who is making it. Enabled RLS with a TRUE condition is equivalent to no RLS at all. The green shield appears either way.

Combined with the service role key in the page source, the entire database was readable and writable by anyone who knew to look.

Finding 4: The OAuth Redirect Was Misconfigured

The Okta integration used window.location.origin for the post-authentication redirect URI, which resolves to localhost in development environments. The issuer URL was also set to the Okta admin console domain rather than the actual tenant domain, causing OIDC discovery to fail silently.

What Was Fixed

The service role key was replaced with the publishable key. Okta was connected as an OIDC provider via Supabase Auth — the application now redirects unauthenticated users to Okta automatically on load. RLS policies were rewritten to require auth.uid() IS NOT NULL, and all grants to the anonymous role were revoked. The redirect URI was hardcoded to the production URL and the correct Okta issuer domain was confirmed against the OpenID configuration endpoint.

The application is now in production.

A Note on Supabase Specifically

None of these are Supabase bugs. The platform works as documented. But the combination of a client-facing API key, auto-generated REST endpoints, and dashboard indicators that look correct without necessarily being correct makes it easy to ship something that appears secure but isn’t.

The green RLS shield is the clearest example. It indicates that RLS is enabled, not that the policies are effective. Those are different things, and the dashboard doesn’t distinguish between them.

If you’re running a Supabase project, three things worth checking:

  • Which key is in your frontend code – service_role should never be there
  • Whether your application loads data before a user has authenticated
  • What the actual condition is in each RLS policy, not just whether the shield is green

The Bigger Question

This review happened because the owner asked for it before going live. That’s not common. Most internal tools get built, get used, and never get looked at from a security perspective until something goes wrong.

The vulnerabilities here weren’t sophisticated. They required no special knowledge to find or exploit. They existed because the focus during development was on making the application work — which is reasonable — and security was treated as something to come back to.

The question worth asking, for any application handling data that matters: when does “come back to it” actually happen?

If you would like us to take a look at something you’re running, get in touch.

Leave a Reply