AI-coded apps

    Is my Supabase anon key safe in a public Replit repo?

    Code editor showing a Replit project pushed to a public GitHub repository with the Supabase URL and anon key visible in a .env file

    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.

    CredentialWhere it belongsWhat protects you if it leaks
    anon / publishable keyClient code, public repos, browser bundlesRow Level Security policies on every exposed table
    service_role / secret keyBackend code, Edge Functions, never the clientNothing once leaked; rotate immediately and audit logs
    Database password.env for psql sessions, CI secrets onlyNothing once leaked; rotate in Project Settings, Database
    JWT signing keyStored by Supabase, used to sign tokensRotation 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.relrowsecurity check across the public schema, and a full select * from pg_policies to confirm every policy actually filters by auth.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.
    • #supabase
    • #anon key
    • #rls
    • #replit
    • #postgres
    • #ai security
    • #pre-submit

    Frequently asked questions

    What is the difference between the Supabase anon key and the service role key?
    The anon key authenticates a request as the anon Postgres role, which has no special privileges and is fully subject to Row Level Security policies. The service role key authenticates as the service_role Postgres role, which carries the BYPASSRLS attribute and ignores every policy on the database. The anon key belongs in client code and public repos. The service role key belongs only in server code, Replit Secrets used by a backend, or a Supabase Edge Function.
    Will Supabase rotate my anon key if it leaks?
    Supabase does not rotate the anon key automatically, because the key is not treated as a secret. You can rotate it manually from Project Settings, API, but rotation invalidates every existing client and forces a full redeploy. The cleaner path is to leave the key in place, verify Row Level Security covers every table in every exposed schema, and treat the key as a public identifier from the first commit forward.
    Does enabling RLS in Supabase Studio also protect tables outside the public schema?
    No. RLS is a per-table flag, and Studio only enables it on the tables you toggle. Tables in custom schemas like private or app_internal may not even be exposed through the PostgREST API by default. Check the API, Exposed Schemas setting in Project Settings to see which schemas the anon key can reach, then audit RLS on each table inside those schemas with a query against pg_class.relrowsecurity.
    Should I still keep my anon key in a .env file even though it is safe?
    Yes. The Supabase docs and the team in public discussions both note there is no upside to publishing the anon key directly in source, even when it is technically safe. A .env file plus a documented .env.example is the standard pattern. It also makes the eventual migration to the new publishable keys easier, because each environment can hold its own value without a code change in the build output.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free