AI-coded apps

    Why does Replit Agent return 403 from Supabase on mobile?

    A Replit Agent chat window beside an Expo mobile preview showing a Supabase 403 error in the console

    You ran a Replit Agent build, wired up Supabase, opened the mobile preview, and the first call to your database returns HTTP 403. The auth flow works, the SQL works in the editor, and the same code worked yesterday in the web preview. The fix is rarely one thing, so the first move is to identify which of four common causes is in play.

    Short answer

    A 403 from Supabase in a Replit Agent mobile build almost always traces to one of four problems: row level security is enabled on the table without any policies, the anon key is referenced by the wrong environment variable name, the JWT is missing or stale on the outgoing request, or an SSL or CORS misroute on Replit's preview domain prevents the request from reaching Supabase with the right headers. Check the response body for a PostgreSQL 42501 code first, then work the four causes in order.

    What you should know

    • A bare 403 with PostgreSQL code 42501 means RLS, not network. When the response body includes "code":"42501", the row level security policy is denying the request before any data leaves the database.
    • Replit Agent enables RLS automatically when it provisions Supabase, but does not always write every policy. Tables created later in the session can land with RLS on and zero policies, which denies all access by default.
    • anon and authenticated are the two Postgres roles Supabase maps client requests to. Policies must target the correct role with TO authenticated or the request fails for logged-in users.
    • Expo apps need environment variables prefixed with EXPO_PUBLIC_. Anything else stays undefined at runtime, so the client falls back to an empty key and Supabase responds with 401 or 403.
    • An UPDATE policy needs both USING and WITH CHECK. Omitting either side returns 403 for valid users editing their own rows.

    Why does Replit Agent leave Supabase tables with broken RLS?

    The short answer is that Replit Agent ships builds quickly and trusts you to verify the security layer before launch.

    According to Replit's Expo tutorial and Supabase's API security guide, Supabase exposes the public schema through PostgREST by default, and any granted table without RLS enabled can be read by the anon role with matching API grants. When the Agent's prompt explicitly mentions Supabase, it usually provisions a project, sets up auth, and enables RLS on the tables it generates. That part is safe. The unsafe part is the second wave of tables the Agent adds during a long session. Each new migration can land with RLS on and no SELECT, INSERT, or UPDATE policy, which means every request from the mobile client returns 403.

    The pattern is documented in the Supabase Row Level Security guide: when RLS is enabled and no policy exists for a given role and command, Postgres returns a permission denied error. From the mobile side, that surfaces as a 403 with body {"code":"42501","message":"permission denied for table ..."}. The Replit Agent does not flag this in the chat, so the first hint comes from the mobile log.

    The correct response is to enumerate the tables the Agent touched in the session, check which ones have RLS on, and add the missing policies. Disabling RLS removes the safety net entirely, which is rarely what you want on production.

    How do I check whether the 403 is really an RLS problem?

    The short answer is to read the response body of the failed request, not just the status code.

    A 403 with code: 42501 is an RLS denial. A 403 with message: "Invalid API key" or "JWT expired" is an auth header problem. A 403 with no Supabase body at all (just the platform's default error page) usually means the request was blocked before it reached Supabase, which points to a Replit preview proxy, a Capacitor or Expo origin issue, or an SSL certificate that the mobile WebView refuses to trust.

    In an Expo or React Native app on a Replit Agent build, the easiest way to read the body is to wrap the Supabase call and log error.code, error.message, and error.details directly. The Supabase client returns those fields on every failed query. If error.code is 42501, stop debugging headers. If it is PGRST301 or similar, the issue is JWT or grants. The diagnosis takes thirty seconds and saves an hour of policy rewriting that was not needed.

    Symptom in the mobile logMost likely causeFirst fix to try
    403 with "code":"42501"RLS enabled, no matching policyAdd SELECT or INSERT policy for authenticated
    403 with "message":"Invalid API key"Anon key missing or misnamedCheck EXPO_PUBLIC_SUPABASE_ANON_KEY
    403 with "message":"JWT expired"Stale session, no auto refreshSet autoRefreshToken: true on client
    403 served as a Replit HTML pagePreview proxy or SSL trust issueTest against production supabase.co URL directly
    403 on UPDATE onlyMissing WITH CHECK clauseAdd WITH CHECK (auth.uid() = user_id)

    What environment variable does Replit Agent expect for the Supabase anon key?

    The short answer is EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY (or EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY in newer key naming) for Expo-based builds, set in Replit's Secrets pane.

    According to the Supabase Expo React Native quickstart, Expo requires environment variables to be prefixed with EXPO_PUBLIC_ to be accessible in the app at runtime. Anything else stays undefined when the bundle is built. A common Replit Agent slip is naming the secret SUPABASE_ANON_KEY (no prefix) because that name reads cleanly. The web preview happens to work because Next or Vite resolves it through a different convention, then the mobile build silently falls back to undefined, and the Supabase client sends a request with no apikey header. Supabase responds with 401 or 403 depending on which check fails first.

    The verification step is to log process.env.EXPO_PUBLIC_SUPABASE_URL at the top of the entry file. If the log line is empty, the variable name or the bundling setup is the issue, not RLS.

    A second slip is sending both an apikey header and an Authorization header where the Authorization JWT is malformed. The Supabase troubleshooting docs note that Authorization takes priority, so a bad JWT on a request that has a perfectly valid apikey still returns 403. In a Replit Agent app, this can happen when the auth flow is half wired and the session token is null but the client still sets an Authorization header on some calls.

    How do I rebuild the missing RLS policies correctly?

    The short answer is to add one policy per table per command for the authenticated role, with the right combination of USING and WITH CHECK.

    For a posts table with a user_id column:

    alter table public.posts enable row level security;
    
    create policy "Users can read their own posts"
      on public.posts for select
      to authenticated
      using (auth.uid() = user_id);
    
    create policy "Users can insert their own posts"
      on public.posts for insert
      to authenticated
      with check (auth.uid() = user_id);
    
    create policy "Users can update their own posts"
      on public.posts for update
      to authenticated
      using (auth.uid() = user_id)
      with check (auth.uid() = user_id);
    

    Two patterns trip up Replit Agent builds in particular. The first is forgetting WITH CHECK on UPDATE. The GitHub issue thread on Supabase RLS 403 errors covers this in detail: an UPDATE policy needs both clauses because Postgres validates the row before and after the change. The second is policies that target the default public role rather than authenticated, which works in some queries and fails in others. Target authenticated explicitly.

    Once the policies exist, the test that actually proves they work is a runtime check, not a scanner pass. Send a request with no JWT and confirm the response is empty. Send a request with one user's JWT and try to read another user's row. If both return empty, the policy is doing its job. If either returns data, the policy lets too much through.

    What about SSL certificate errors on a Replit-deployed Supabase call?

    The short answer is that the Supabase URL itself is fine. The SSL trouble usually sits on Replit's custom domain layer or on the mobile WebView.

    Replit's published status history lists several incidents where custom domain SSL certificates failed to issue or expired, which produces a network-level rejection in any mobile WebView before the call ever reaches Supabase. The mobile log shows a generic 403 or an NSURLErrorDomain failure with no Supabase body. The way to confirm this is to fire the same Supabase call directly against the project's https://*.supabase.co URL, not through a Replit-hosted proxy. If the direct call works, the issue is the deployment layer.

    For Capacitor builds, an extra step is needed: Android WebView rejects certain TLS chains under stricter network security configs, so the network_security_config.xml should explicitly trust the Supabase host (or the WebView should use the default config when the API is on supabase.co).

    Can I avoid this whole class of bug on the next Replit Agent build?

    The short answer is yes, with three habits that take less than ten minutes per session.

    First, ask the Agent at the end of every Supabase-touching session to list every table it created or modified, and to print the RLS status and policy count per table. The Agent answers honestly when asked directly. Anything with RLS on and zero policies is a 403 waiting to happen.

    Second, store the Supabase anon key under EXPO_PUBLIC_SUPABASE_ANON_KEY (or the new publishable key name) in Replit Secrets, and log the variable once at app start during testing. If it ever logs as undefined, the bundling is broken.

    Third, before any submission to App Store Connect or Google Play, run a separate review against the OWASP Mobile Application Security Verification Standard. For builders who want an automated external read of the compiled APK or IPA before submission, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning of AI-coded mobile builds aligned with OWASP MASVS.

    What to watch out for

    The first trap is disabling RLS to make the 403 go away. That moves the problem from "no users can read" to "any anon user can read everything", which is a much worse failure on production. Leave RLS on and add the missing policy.

    The second trap is treating a 403 from the platform's HTML error page as a Supabase 403. They share a status code and nothing else. The mobile log should show a JSON body from Supabase if the request actually reached the database.

    The third trap is copying example policies that use USING (true). The policy exists, the scanner shows it as enabled, and every row is readable. The Supabase RLS guide is explicit that policies should encode the access rule, not just satisfy the presence check.

    Key takeaways

    • A 403 from Supabase on a Replit Agent mobile build has four common causes: missing RLS policies, a wrong environment variable name, a bad or missing JWT, or a Replit deployment SSL issue. Read the body before changing the database.
    • UPDATE policies need both USING and WITH CHECK clauses with the correct role target (authenticated for logged-in users).
    • The mobile build needs EXPO_PUBLIC_ prefixed secrets to see the anon key at runtime; a missing prefix produces a silent 401 or 403.
    • Runtime testing (a logged-out request, a cross-user request) is the only check that proves an RLS policy actually denies what it should.
    • Some teams pair an AI-coded build with an external pre-submission scanner. PTKD.com (https://ptkd.com) is one of the platforms focused on automated scanning of compiled mobile builds against OWASP MASVS before they reach App Store Connect or Google Play.
    • #replit
    • #replit agent
    • #supabase
    • #error 403
    • #row level security
    • #rls
    • #mobile app
    • #ai-coded apps

    Frequently asked questions

    Does disabling RLS fix the 403 from a Replit Agent Supabase setup?
    Technically yes, the 403 disappears. Practically no, because disabling RLS lets any anon client read every row in the table. The actual fix is to keep RLS on and add the missing SELECT, INSERT, and UPDATE policies for the authenticated role. Anything else moves the failure mode from a denied request to silent data exposure, which is much worse for an app that holds user data.
    Why does my Replit Agent web preview work but the mobile build returns 403?
    The web preview likely reads the Supabase URL and key through a Vite or Next environment convention, while the Expo or React Native bundle needs variables prefixed with EXPO_PUBLIC_. If the key is named without that prefix, the mobile build receives undefined at runtime, sends a request with no apikey header, and Supabase returns 401 or 403. Log the env variable at app start to confirm.
    Is a 403 always row level security, or can it be the Replit deployment layer?
    Either is possible. A 403 with a Supabase JSON body and code 42501 is RLS. A 403 served as a Replit HTML page, or paired with a TLS or NSURLErrorDomain error in the mobile log, is a deployment or SSL issue. Test the same call against the supabase.co URL directly to separate the two. If the direct call works, the deployment layer is the cause.
    Do I need a separate WITH CHECK clause on every UPDATE policy?
    Yes, for any UPDATE policy that should validate the modified row. USING decides which rows the user can see and target, WITH CHECK validates the row after the change. Without WITH CHECK, a valid user editing their own row can still receive a 403 because Postgres re-validates the result through the policy. Both clauses should reference the same condition.
    Will Replit Agent fix the missing policies if I ask?
    In practice, yes, when the request is specific. Ask the Agent to list every table that has RLS enabled with zero policies, then ask it to write SELECT, INSERT, UPDATE, and DELETE policies for the authenticated role on each one, referencing the user_id column. Review the output before running, because the Agent occasionally writes USING (true) policies that satisfy the count without enforcing access control.

    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