deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Missing Supabase Row Level Security turns shipped anon keys into open databases

A dev.to post explains why the Supabase anon key inside a mobile app bundle is not a secret, and why tables without Row Level Security remain readable to strangers.

Missing Supabase Row Level Security turns shipped anon keys into open databases

Anon keys are public by design

The publishable anon key that a Supabase-backed mobile app relies on is not a credential anyone can keep secret. According to a post on dev.to, the key sits inside the client bundle — an Expo build, for example — and anyone can extract it and start calling the database directly. A web app can hide privileged keys on a server; a mobile app has no such hiding place.

Row Level Security (RLS) is the mechanism that makes this arrangement safe. It runs inside Postgres and applies to every query regardless of which client or script sent it. When configured correctly, signed-in users see only their own rows and anonymous callers see only genuinely public data. When it is missing, the post warns, an app can pass every test in development while its tables remain readable to any caller in production — with no error to signal the leak.

Two gates: grants and policies

Supabase maps requests made with the anon key to either the anon role or the authenticated role. Access control then works through two layers. Grants are coarse permissions that decide whether a role may run select, insert, update or delete on a table at all. Policies are row-level rules that behave like an invisible WHERE clause, typically comparing an owner column against the user id embedded in the JWT.

The post stresses that both layers have to be right. A policy without a matching grant fails closed and produces a permission error. A grant without a policy fails open and returns rows. AI-generated schemas, the author argues, tend toward the second failure mode: tables created quickly, RLS never switched on, default grants left broad. The recommended habit is uniform — every table in an exposed schema gets RLS enabled, minimal grants, and at least one policy for each operation the app actually performs, with no exceptions for lookup tables or early prototypes.

Owner-based access as the default

For user data, the post recommends a simple owner pattern. Give each table a user_id column that defaults to the requesting user, so the database rather than the client records ownership from the JWT. Policies then compare that column against auth.uid(), with a using clause filtering reads, updates and deletes, and a with check clause validating writes. Keeping the rule symmetric prevents a user from inserting rows they could never read back or altering someone else's row by guessing its id.

Testing matters as much as configuration. The author advises checking the negative path explicitly: sign in as one user, then attempt to read, update and delete another user's row by id — all three should be denied or return nothing. A suite that only confirms owners can reach their own data has covered half the policy.

Split public reads from private writes

Mixed-access tables, such as public profiles that only their owner can edit, should get separate narrow policies per operation rather than one permissive rule. A select policy should name the anon role explicitly, and only for genuinely public data. Everywhere else, the post suggests revoking anon's grants entirely, treating the absence of a grant as a deliberate restriction rather than an oversight. The service_role key, which is built to bypass RLS, belongs only in server-side code; privileged operations triggered from mobile flows should be routed through edge functions instead of widening a client policy.

Workspaces, performance and bypasses

Shared resources such as team projects need a membership table that policies join through, checking whether the signed-in user belongs to the row's workspace. Because RLS runs on every row a query touches, the post warns that an unindexed membership check turns into a slow app, and recommends indexes on workspace_id and user_id. It also flags a common blind spot: views and functions that execute with elevated privileges can sidestep the policies protecting the base tables. Each should either run as the caller or carry equally strict rules of its own, and the author suggests a short audit listing every view and function that touches user data.

Why it matters

AI-assisted development makes backends feel finished as soon as rows start flowing, and Supabase's design assumes the client key is public knowledge. That makes RLS the only real boundary between an app's data and anyone who pulls the key out of a bundle — and the failure mode is silent, with no crash or error, just permissive tables answering strangers. For any mobile app shipping a publishable key, the checklist in the dev.to post — RLS on every table, minimal grants, a policy per operation, negative-path tests, and an audit of views and functions — is the difference between a working app and an open one.

  • #supabase
  • #postgresql
  • #row-level-security
  • #mobile-security
  • #app-development