Guide
Why did my Supabase login stop working after an AI edit?
Sign-in worked, an AI edit shipped, and the form now spins, bounces back, or lands on the wrong host. The password is usually still right. The edit broke the contract between the code and the project: the public key, the redirect allow list, email confirmation, a row-level security policy, the session client, or the OAuth callback. Read the failing request before you change the dashboard. Editing the app does not, by itself, change Supabase settings.
This is not a launch checklist. For signup, login, and checkout on a published Lovable URL, use how to test a Lovable app before launch.
- Read the failure in the browser
- Project URL or public key
- Site URL and redirect URLs
- Email confirmation
- Row Level Security after sign-in
- A second client, or the session listener
- OAuth callback route
- Keep the next edit from doing this
Read the failure in the browser first
Use the published URL in a private window. The preview is often already signed in. The table editor skips your policies, so a row you can see there does not mean login works.
- Submit a user you know exists. Filter the network panel to the Supabase host, your-project.supabase.co.
- Open /auth/v1/token. A 200 means Auth issued a session, so the break is later. A body that says the API key is invalid means the wrong key, or no key. A body that says the email is not confirmed means the account is waiting on the link. A body that says the login credentials are invalid means this project rejected that email and password. Supabase uses that same sentence when the email is not in this project, so the wrong project looks like a bad password.
- If the token call is 200 and the login page remains, open the next /rest/v1/ request. An empty array, or PostgREST code PGRST116, means the query after sign-in did not return exactly one row.
- Copy the path you landed on, any error or error_description in the address bar, and the first red console line. Match those to the section below.
The project URL or the public key no longer matches
The browser client needs the project URL and a public key: the legacy anon key, which is a JWT, or the publishable key, which starts with sb_publishable_. The service_role key and the secret key, which starts with sb_secret_, stay on the server. They bypass row-level security. Putting one in the browser is not a login fix.
- Under Project Settings, then API Keys, compare the project URL with the host on /auth/v1/. A different subdomain is a different project, including one the editor created while rewriting auth.
- The apikey header has to be that project's anon key or publishable key. A key from the other project, a truncated JWT, or a placeholder such as your-anon-key fails. If legacy keys are turned off, the anon JWT is rejected and the client must send the publishable key.
- Search for createClient and createBrowserClient. Take the URL and the key from the environment, not from a string the edit hard-coded.
- Next.js inlines only names that start with NEXT_PUBLIC_. Vite inlines only names that start with VITE_. Renaming NEXT_PUBLIC_SUPABASE_ANON_KEY to SUPABASE_ANON_KEY leaves the browser value empty while the server still has it. Those values are copied at build time, so redeploy after you correct them.
- An empty URL throws that the Supabase URL is required. A value that is not http or https throws that the URL is invalid. An empty key throws that the key is required.
Site URL and redirect URLs no longer match the code
Auth only sends a browser, a confirmation email, or a password-reset email to an allowed URL. The code passes it as redirectTo or emailRedirectTo. A new callback path or origin fails even when the dashboard was not edited. Magic links use the same Site URL and the same list. The allow-list version of this break is also covered in login works locally but breaks in production.
- Open Authentication, then URL Configuration. Site URL should be the production origin, including https. Left on http://localhost:3000 or a preview host, the emails go there.
- Every redirectTo and emailRedirectTo value has to match a Redirect URLs entry, including https and the path. A wildcard uses an asterisk, and an entry for subpaths usually ends in /**. https://yourapp.com/auth/callback does not match a different path, or a trailing slash the edit added.
- Search the repo for redirectTo and emailRedirectTo and compare them to the list. Read the host in a confirmation or reset email before you click. Localhost means Site URL or emailRedirectTo is still the old origin.
- If the response says the redirect URL is not allowed, add the exact URL the app asked for. Do not open the list to every host until you know that string.
Email confirmation no longer matches the form
Confirm email is under Authentication, then Providers, then Email. When it is on, signUp returns a user and no session until the link is clicked. When it is off, signUp can return a session and no email is sent. A code edit does not flip the toggle. A pushed Supabase config file, supabase/config.toml, can. Read the toggle.
- Sign up with a new address in a private window. Under Authentication, then Users, an empty confirmation time means there is no session yet. A screen that walks straight into the product looks logged out, and signInWithPassword reports that the email is not confirmed.
- If the toggle is off and the screen waits for email, the edit added a step the project will not send. Use the session signUp returned, or turn confirmation on and show a check-your-email state when the session is null.
- If no message arrives, open Logs, then Auth, before you rewrite the client. The built-in sender is for early testing and is rate-limited, so a burst of attempts sends nothing. Custom SMTP failures show up in those logs.
- Read the function, not the button. signInWithOtp on a password form sends a magic link and no session. signInWithPassword after the password field was removed never reaches Auth with a password.
Row Level Security hid the row the app reads after sign-in
Auth can return 200 and the app can still bounce. Generated apps load a profile and treat zero rows as logged out. Row Level Security filters that read. The table editor does not, so a visible row can still be invisible to the signed-in user.
- Read the table name on the /rest/v1/ request. A body of [] means no policy matched. .single() on zero rows, or on more than one, fails with PGRST116, which means a single JSON object was requested and the result was not one row.
- The select policy has to allow the authenticated role. A policy only for anon does not apply to a session. The usual expression compares auth.uid() to the column that stores the user id. auth.uid() = user_id matches nothing when the column is id. auth.uid() is a uuid, so a comparison to text with no cast fails the same way. RLS with no policy for that role returns no rows. A policy for public applies to every role. Replacing it with a broken authenticated expression hides the row.
- If the row is missing in the table editor too, the insert never ran. Starter SQL copies auth.users into a profile with a trigger. That function is marked security definer and writes public.profiles, because the search path is empty. Dropping the trigger, security definer, or the schema prefix lets sign-up succeed and leaves no profile.
The client was created twice, or the listener redirects too early
A token response of 200 means Auth issued a session. The app still has to write it and read the same store. A console warning that starts with Multiple GoTrueClient instances detected in the same browser context means two auth clients are alive.
- Keep one browser factory. createBrowserClient from @supabase/ssr reuses a client for the same URL and key, and stores the session in cookies. A second createClient from @supabase/supabase-js stores it in localStorage under a key like sb-your-project-ref-auth-token. The page that reads the other store sends you back to the form. A custom storageKey does the same. Do not construct the plain client in the component body. That builds another instance on each render.
- If the token call succeeded and the UI still leaves, wait for onAuthStateChange (INITIAL_SESSION or SIGNED_IN) or for getSession before you redirect. Do not await another Supabase call inside that callback. The client holds a lock there, and a nested call can deadlock, which looks like a spinner. getSession reads stored JSON. getUser checks the token with Auth. Swapping them does not write a missing cookie.
- Signed in until you reload means the server never saw the cookie. With @supabase/ssr, the server client has to use the request cookies, and middleware or proxy has to call getUser so the refresh cookie can rotate. Remove that call and the session dies when the access token expires. The default JWT lifetime is one hour, unless you changed it.
The OAuth callback route moved
Google, GitHub, and the other providers redirect to Supabase, not to your page. On a hosted project that URL is https://your-project-ref.supabase.co/auth/v1/callback. Supabase then sends the browser to your redirectTo. Current @supabase/supabase-js v2 clients use PKCE unless flowType is implicit, so the route gets a code query parameter and must call exchangeCodeForSession. Moving the file, or only reading the URL hash, drops the session.
- If the browser never leaves your site, the client failed first. Use the console line from the first section.
- A 404 on your domain means redirectTo points at a path that is gone. Point it at the route that calls exchangeCodeForSession, and add that exact URL under Authentication, then URL Configuration, then Redirect URLs.
- A code in the address bar, then the login page, means the code was not exchanged, or the exchange ran on a server client that did not write cookies. Exchange and cookie write belong in the same request.
- In Google Cloud the authorized redirect URI, and in GitHub the authorization callback, stays the Supabase URL above. Replacing it with your /auth/callback fails at the provider, before your page loads.
- flowType implicit against a route that expects code, or the reverse, will not finish. detectSessionInUrl set to false makes the browser client ignore a session in the URL. Leave the default unless the server callback consumes the code.
How to keep the next edit from doing this
- After any prompt that touches auth, environment files, SQL, or routes, use a private window on the published URL. Sign up with a new email, finish the confirmation link if the toggle is on, log out, log in, reload, and open one page that requires a session.
- In the diff, look for createClient, createBrowserClient, redirectTo, emailRedirectTo, onAuthStateChange, exchangeCodeForSession, create policy, and security definer.
- Write down the production origin, the callback path, and whether Confirm email is on. Change the dashboard in the same sitting as the code. Keep a Supabase client rewrite out of an unrelated feature prompt, so the diff stays small enough to check.
The free scan only loads the page. It does not type a password, open an email, or follow a provider onto another host. To rerun the steps on your site, create an account and describe the flow, such as “Sign up with a new email, then open the dashboard.” Test emails end in @example.com. The watch stays on your hostname, skips email codes and passkeys, and emails the failed step plus a prompt you can paste back into the editor.
Starter is $19/mo. Pro is $49/mo. What each plan includes, and how a watched flow compares with testing by hand, is on the facts page. FlowPatrol is not affiliated with Supabase.