You vibe-coded a project in Replit, hooked it up to Supabase, and now you are about to flip the repo from private to public so you can share the deploy link. The .env file has your Supabase URL and the anon key sitting there, and a small voice is asking whether you just shipped a credential to the internet. The short version is that the anon key is meant to be public. The longer version is that the safety only holds when Row Level Security is configured correctly on the database behind it.
Short answer
The anon key (now branded as the publishable key in the new Supabase API key model) is designed to be visible in client code, Git repos, and browser network tabs. According to Supabase's API keys documentation, the key is "safe to expose online: web page, mobile or desktop app, GitHub actions, CLIs, source code." The risk is not the key string itself. The risk is a table in the public schema with Row Level Security disabled, or the service_role key sitting in the same Replit project by accident.
What you should know
- The anon key is a JWT for the anon Postgres role. It carries no privileged claims and cannot bypass Row Level Security policies on its own.
- Row Level Security is the real gate. A table in the public schema with RLS disabled returns every row to any holder of the anon key.
- The service_role key is the dangerous one. It carries the BYPASSRLS attribute and must never appear in a public repo or a client bundle.
- Supabase is moving to publishable and secret keys in 2025-2026. The rollout is gradual through late 2026, and the safety logic stays the same.
- Scrapers index public repos for Supabase URLs. Even when the anon key is safe, there is no value in making your project trivially discoverable to attackers probing for tables with RLS off.
What does the Supabase anon key actually do?
The short answer is that the anon key is a JSON Web Token signed by your project that authenticates a request as the anon Postgres role. PostgREST, the HTTP layer in front of Supabase, reads the token, sets the database session to the role named in the token, and runs the query. The anon role has no superuser attributes, no BYPASSRLS flag, and no SELECT or INSERT privileges that ignore policy checks.
That means the key on its own does nothing dangerous. The key proves the request reached the right project. The database then applies whatever policies exist on the table being queried. According to Supabase's API keys guide, the anon key model is the same shape as a Firebase Web API key or a Stripe publishable key: the string identifies the project, and authorization happens elsewhere against rules you wrote.
Row Level Security is that rule layer. Supabase's Row Level Security documentation is explicit on the contract: once RLS is enabled, no data is accessible via the API when using a publishable key until you create policies. For the anon key to remain harmless, three things have to be true at once. Every table exposed through the API has ENABLE ROW LEVEL SECURITY set. Every policy uses an expression that actually restricts what an anonymous caller can read. No view, function, or RPC wrapper around those tables runs as a higher-privileged role that ignores the policy layer.
What happens if I forget RLS on one table?
The honest answer is that the table becomes a public read endpoint, and sometimes a public write endpoint. The Supabase database advisor flags this case as rls_disabled_in_public because it is the most common foot-gun in the platform.
A concrete walkthrough. A developer builds a Replit chat app and creates a messages table in Supabase Studio. ENABLE ROW LEVEL SECURITY is left off, because the plan is to "come back to it." The Replit project pushes to a public GitHub mirror with the anon key in .env. An attacker with curl, the project URL, and the anon key can now run:
curl "https://<project-ref>.supabase.co/rest/v1/messages?select=*" \
-H "apikey: <anon-key>" \
-H "Authorization: Bearer <anon-key>"
The response is every row in the table. There is no auth flow. There is no per-table rate limit on this path. Public Supabase incidents reported in 2024 and 2025 followed this exact pattern, where indexed repos and Shodan-style scans turned up projects with RLS disabled on user-data tables. The fix in each case was the same: enable RLS, write policies that filter by auth.uid(), and re-run the audit against the full schema.
How do I check my Replit project before going public?
The short answer is that two checks settle it. First, run an RLS audit against your Supabase project. Second, scan the Replit workspace for the service_role key.
For the database, connect with psql or use the SQL editor in Supabase Studio and run:
select n.nspname as schema,
c.relname as table_name,
c.relrowsecurity as rls_enabled
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind = 'r'
order by c.relname;
Any row that returns rls_enabled = false is a hole. The next query checks that the policies on those tables actually filter, instead of accepting everything:
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename, policyname;
A row where qual = 'true' and roles = {anon} is a policy that exists but allows the whole internet to read. The Supabase CLI ships supabase db lint --schema public --level warning, which flags both cases as part of its database advisor lints.
For the Replit side, the secret you want absent is the service_role JWT. Look in .env, .replit, replit.nix, and any committed config under ~/secrets. The service_role key is typically a JWT around 200 to 250 characters with the role claim set to service_role. If it sits inside the project directory and that directory is in the public mirror, you have a different problem than the one you came here for.
What about the service_role key in Replit secrets?
The honest answer is that Replit Secrets is the right place for the service_role key, but only when the code that reads it runs server-side and does not bundle those secrets into the client. Replit secrets are environment variables exposed to the running process. Front-end frameworks that build a static client bundle (Vite, Next.js export, Create React App) can embed environment variables into the shipped JavaScript if you name them with the wrong prefix.
The Supabase JavaScript client takes two arguments for an admin client: the project URL and the service_role key. Any code path that calls createClient(url, serviceRoleKey) and runs in the browser is a critical vulnerability. The service_role key carries the BYPASSRLS attribute, so it ignores every policy in the database. Supabase added a User-Agent check on the new secret keys that returns HTTP 401 when the request comes from a browser, but legacy service_role JWTs do not have that defense. Until your project migrates to the new key model described in Supabase's JWT signing keys announcement, the only safeguard is your own discipline about where that key lives.
The table below sorts the four credential types a Replit-to-Supabase project typically touches, where each one belongs, and what protects you when one leaks.
| Credential | Where it belongs | What protects you if it leaks |
|---|---|---|
| anon / publishable key | Client code, public repos, browser bundles | Row Level Security policies on every exposed table |
| service_role / secret key | Backend code, Edge Functions, never the client | Nothing once leaked; rotate immediately and audit logs |
| Database password | .env for psql sessions, CI secrets only | Nothing once leaked; rotate in Project Settings, Database |
| JWT signing key | Stored by Supabase, used to sign tokens | Rotation through Project Settings invalidates all tokens |
What to watch out for
The biggest trap is treating the dashboard's green RLS checkmark as confirmation that the table is safe. ENABLE ROW LEVEL SECURITY and CREATE POLICY are independent states. A table can show RLS as enabled with a policy attached whose USING clause is true, which lets anonymous users read every row. The advisor lint rls_enabled_no_policy catches the inverse case (RLS on, no policies, table returns zero rows) but not the dangerous one.
The second trap is public schema spillover. A side table created for caching, analytics, or feature flags often lands in public because that is the default schema. The anon key reaches it. If RLS is off on that table, anyone with the anon key can read it.
The third trap is the public view that ignores RLS. Postgres views run with the privileges of the view owner by default. A view in public over a private table can leak rows the underlying RLS would have blocked. Supabase recommends security_invoker = true on views that wrap RLS-protected tables.
The fourth trap, less technical, is that even when the anon key is safe by design, public repos with SUPABASE_URL and SUPABASE_ANON_KEY in plain text are easy targets for scripted scanners. A Supabase team member noted in GitHub discussion 22028 that there is no benefit to making the URL trivially indexable. Use a .env file, commit a .env.example with the variable names, and let collaborators bring their own project keys.
Key takeaways
- The Supabase anon key is designed to be public. Safety comes from Row Level Security on every exposed table, not from hiding the key string.
- Run two queries before you flip a Replit repo public: a
pg_class.relrowsecuritycheck across thepublicschema, and a fullselect * from pg_policiesto confirm every policy actually filters byauth.uid()or a similar claim. - Never put the service_role key in client code or a public repo. The new secret keys reject browser requests; legacy service_role JWTs do not. Migrate ahead of the late-2026 cutoff in the Supabase rollout.
- Treat the dashboard's green RLS checkmark as a signal, not a verdict. Confirm the USING and WITH CHECK expressions on each policy in psql before publishing.
- Some teams pair the database-side RLS audit with an external scan of the compiled mobile or web build that talks to Supabase, to catch hardcoded service_role keys or other patterns that bypass RLS at the client. PTKD.com (https://ptkd.com) is one of the platforms focused on that pre-submission check.




