Skip to content
FlowPatrol

Guide

Why does my signup button do nothing after an AI edit?

Sign up used to create an account. After an AI edit, the button still looks the same and the click does nothing. The form stays put. There is no toast, no red text, and often no new row in the network panel. Lovable, Bolt, v0, Cursor, and Replit apps hit this when a prompt rewrites the form, the auth client, or the profile insert, and the failure is no longer shown on the page.

This is not a launch checklist. For a first pass on a published Lovable URL, use how to test a Lovable app before launch. If Auth answers and login still fails, use why Supabase login stopped working after an AI edit. If the account exists and the confirmation never arrives, use why Supabase sign-up confirmation emails are not arriving. Google returning to localhost is why Google sign-in redirects to localhost after deploy. A 404 from a renamed path is when an AI refactor renames a route. A signup page that bounces to login is when AI-added auth middleware locks out public pages. This page is the click that looks dead.

Check the click on the published URL

Use the published URL in a private window. The editor preview is a different origin. It often still has the old environment values, and it is often already signed in. A click that works only in the preview is not a pass. Do not start by asking the editor to rewrite auth.

  1. Open the public signup page. Write down the button words, such as “Sign up” or “Create account”, and the field labels.
  2. Open the browser tools. Select the Network panel. Turn on Preserve log so a reload does not clear it. Filter to Fetch/XHR. Clear the list. Open the Console and turn on Preserve log there too.
  3. Click the button once. Wait. A second click hides which call belonged to the first.
  4. No new row, including no call to your own /api or /signup route, to your-project.supabase.co, or to identitytoolkit.googleapis.com: the handler never ran. Use the no-request section. A document request that only reloads this page is the same section.
  5. A row is 4xx or 5xx and the form does not change. Copy the status and the body. Use the swallowed-error section. If /auth/v1/signup is followed by a failed /rest/v1/profiles call, use the profile section too.
  6. The console turns red before any row. A message that the Supabase URL is required, or that the key is required, means the client was built empty. Copy that line and use the environment and key-name sections.
  7. /auth/v1/signup returns 200 and the form does not change. Open Authentication, then Users. A new row means Auth worked, so stop rewriting the button. A failed profiles call is the profile section. A user and a null session means Confirm email is on. Show check-your-email instead of waiting for a session. That wait is the login guide. The message is the confirmation guide.
  8. The bar jumps to login before the click, or the signup route returns 401: a guard is in front of the page. That is the middleware post above, not a missing onClick.

No auth request: the handler, the button type, or onSubmit

A click with no call to signUp, to createUserWithEmailAndPassword, or to your own signup route means that function is no longer attached. The layout can look finished. Search the signup component before you change the Supabase project.

  1. Search for signUp, createUserWithEmailAndPassword, and the route the button used to call. A comment, a console.log, or a TODO where the call was is a placeholder. A handler that only logs, or only fires a success toast, creates no account. Put the real call back.
  2. Read the button. type="button" does not submit a form. It runs onClick only, so with no onClick the click does nothing. type="submit" runs onSubmit. A button with no type inside a form defaults to submit. Setting type="button" while the logic stays on onSubmit disconnects them.
  3. Read onSubmit. preventDefault and then a return, or an empty body, stops the browser and never calls Auth. Call signUp from that function. Remove preventDefault only when the form is a plain post to a route that still exists. onSubmit={handleSignup} does nothing when the function that calls signUp is named handleSubmit and is never passed.
  4. A disabled flag that stays on, or a loading flag set true and never cleared, ignores the click. In Elements, the click target has to be the button. A leftover modal layer, or pointer-events set to none, takes the click and the network panel stays empty.
  5. A 404 to the old path means the handler still ran and the route moved. That is when an AI refactor renames a route. Point this button at the path that still creates the user.

The request failed and the page hid it

supabase-js does not throw when signUp fails. It returns data and error. A rewrite that only uses try/catch never sees that object, the catch stays empty, and the form stays still. The network panel is the only copy of the failure. A fetch to your own route, and Firebase’s createUserWithEmailAndPassword, do reject, and an empty catch hides those too. A prompt to make the form cleaner often deletes the only place the message was rendered.

  1. Find the catch. An empty block, a catch that only writes to the console, or a catch that returns without setting state is why the page looks calm. Render error.message in the form, next to the button.
  2. Find an if (error) return, or an if (!response.ok) return, with no setError and no throw. That is the same silence. The status and body you copied are the message to show.
  3. sonner, react-hot-toast, and a shadcn toast do nothing visible when the layout no longer mounts the toaster. The function still returns, and the page does not change. A deleted FormMessage does the same. Put the sentence back in the form.
  4. Read the body before you change the call. “User already registered” means this address is already in this project. “Invalid login credentials” means the button calls signInWithPassword, not signUp. A line that signups are not allowed means the Email provider, or new signups, are off under Authentication, then Providers. A line that the email rate limit was exceeded means Auth refused another message for now. “Invalid API key” means the key on the request is not this project’s public key. Show that sentence.
  5. A 200 with a user and a null session is not a hidden error. Confirm email is on. Show a check-your-email state when the session is null. Spinning until a session appears looks like a dead button after Auth has already succeeded.

The public environment values never reached the browser

The homepage can load while the auth client has no URL and no key. Next.js copies a name into the browser bundle only when it starts with NEXT_PUBLIC_. Vite copies a name only when it starts with VITE_ and the code reads import.meta.env. Those values are fixed when the site is built. A redeploy that drops them, or a rename that removes the prefix, leaves the browser holding empty strings. The preview can keep working, because preview settings are separate, and the click throws before any request.

  1. Open the environment settings for the deployment that serves the URL you are testing, not the preview. Lovable, Bolt, v0, Replit, and the host behind a Cursor deploy keep those lists apart. A value you can see in the editor is not automatically on the published deployment.
  2. Search for createClient and createBrowserClient. Write down the exact names the call reads. NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY, or NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, are the usual Next.js names. VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY, or VITE_SUPABASE_PUBLISHABLE_KEY, are the usual Vite names. SUPABASE_URL and SUPABASE_ANON_KEY, with no prefix, are empty in the browser even when a server route can read them.
  3. Put the project URL and the anon key, or the publishable key, on those prefixed names for this deployment. Both values have to be from the same Supabase project. Then publish again. Saving the settings does not change the bundle that is already live.
  4. Click again in a private window. If the console no longer says the Supabase URL is required, or the key is required, the bundle saw the values. A request that then returns “Invalid API key” is the next section, not another missing publish.
  5. Do not put the service role key, or a secret that starts with sb_secret_, into a NEXT_PUBLIC_ or VITE_ name to get past an empty client. That key bypasses row level security, and it would ship in the bundle. The browser gets the anon key or the publishable key only.

The client was created with the wrong key name

The host can hold the right value under a different name than the code reads. The edit renamed one side. The other client in the file can still be valid, so the rest of the page looks connected while the click builds a second client and throws.

  1. Compare the string inside process.env or import.meta.env with the name in the deployment settings, character for character. NEXT_PUBLIC_SUPABASE_ANON_KEY does not read NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY or NEXT_PUBLIC_SUPABASE_KEY. VITE_SUPABASE_ANON_KEY does not read NEXT_PUBLIC_SUPABASE_ANON_KEY. A Vite file that still says process.env.NEXT_PUBLIC_… is empty unless that project defined a replacement. A Next file that says import.meta.env.VITE_… is empty.
  2. On the failed request, if one ever leaves, read the apikey header. The literal undefined, an empty header, or your-anon-key means the name resolved to nothing or to a placeholder the edit left in source. A URL of https://your-project.supabase.co is the same kind of placeholder. Take both values from the environment. Do not hard-code them.
  3. Under Project Settings, then API Keys, the legacy anon key is a JWT. The publishable key starts with sb_publishable_. If legacy keys are turned off, the anon JWT is rejected even when the name matches. Send the publishable key. A different subdomain on /auth/v1/ is a different project, including one the editor created while rewriting the client. The rest of that comparison is the public-key section of the Supabase login guide.
  4. Construct the browser client once, from the names this app actually inlines. A createClient inside the submit handler that uses a shortened name throws on click even when the page constructed a good client at load. The console line names the empty argument. The network panel stays empty.

Row level security blocked the profile insert

Auth can return 200 and the button can still look dead. Generated apps insert into profiles, users, or accounts immediately after signUp, and they treat a failed insert as a failed signup. When that error is not rendered, you stay on the form. The auth user is already there. Check Users before you touch the button again.

  1. In the network panel, read the call after /auth/v1/signup. A POST to /rest/v1/profiles, or the table this app uses, with status 401 or 403 and a body that says new row violates row-level security policy is this break. Postgres code 42501 is that sentence.
  2. Open Authentication, then Users, and search the address. If it is listed, stop rewriting signUp. The click reached Auth. Fix the insert.
  3. With Confirm email on, signUp returns no session, so the insert runs as anon. A policy for authenticated, or auth.uid() = id, does not allow it, because auth.uid() is null until the link is clicked. Restore the security definer trigger on auth.users that inserts the profile, or insert only after a session exists. Do not turn Confirm email off to hide the rejection.
  4. With a session, auth.uid() = user_id still matches nothing when the column is id. A service-role-only policy does not apply in the browser. Row level security with no insert policy for the role that writes rejects the row. The table editor bypasses policies, so a row you add by hand does not prove the form can insert one.
  5. A signup that seemed to finish and then shows a blank dashboard is when an AI edit locks new users out of their own data. Reading that profile on a later login is the row level security section of the login guide.
  6. Do not turn row level security off on profiles. That lets one account read another account’s row. Change the insert and select policies for this table, or put the write back on the security definer trigger.

Restore the last good version, then change one thing

When the diff is too wide to see which of the above moved, put the last working signup back before you ask for another full rewrite. A follow-up prompt that says to fix auth tends to replace the client, the policies, and the form in one pass, and the click stays dead.

  1. In Lovable, Bolt, or the host’s deployment history, restore the last published version where a new email still created an account. In git, check out that commit and deploy it. Confirm it on the published URL, in a private window, with an address you have not used in this project.
  2. Click once. You want a check-your-email state or the signed-in screen, and a row under Authentication, then Users. If that older version fails the same way, the break is in the Supabase project or in the host’s environment settings, not in the newest component. Check the prefixed names and the profiles policy before you edit the button.
  3. When the restored version works, reapply only the change you actually wanted. Say in the prompt not to edit the submit handler, the auth client, the environment names, or the profiles insert. Read the diff before you publish.
  4. In that diff, search for onSubmit, onClick, type="button", signUp, createClient, catch, toast, Toaster, profiles, NEXT_PUBLIC_, and VITE_. Any hit is the narrow fix. Publish, then repeat the private-window click and read the network panel again.

A prompt you can paste

Paste this into Lovable, Bolt, v0, Cursor, Replit, or the editor you used, after you have the network row and any console line. Replace the brackets. It keeps the edit on this click.

The signup button on my published app does nothing after the last edit. The form stays put. There is no error on the page. Do not redesign the page. Do not replace the auth provider. Do not turn off row level security. Do not disable email confirmation to hide this.

What I see: [no network request / request fails and no message / console error before any request / profile insert denied / button stays disabled]
Page: [URL]
Button label: [words on the button]
Network row: [none, or method, URL, and status]
Response body or console line: [paste it]
Address I tried: [email]

Fix this click only:
1. Run the existing signup call. A type="button" with no onClick does nothing. A type="submit" whose onSubmit calls preventDefault and then returns does nothing. Wire onSubmit or onClick to supabase.auth.signUp, or to the create-user function already in the app. Remove a placeholder that only console.logs or only shows a success toast.
2. If signUp returns an error, show error.message in the form. supabase-js does not throw on a failed signup, so an empty catch hides it. If the toaster was removed, do not rely on toast.
3. Build the browser client from the project URL and the anon or publishable key. Next.js inlines NEXT_PUBLIC_ names. Vite inlines VITE_ names read from import.meta.env. Do not read SUPABASE_SERVICE_ROLE_KEY, sb_secret_, or an unprefixed SUPABASE_ANON_KEY in client code. Do not prefix the service role with NEXT_PUBLIC_ or VITE_. Leave a comment listing the exact names to set on the production deployment. Do not invent values.
4. Do not treat a failed profiles insert as a silent return. If Confirm email is on, signUp has no session, so an insert as anon fails row level security. Restore the security definer trigger on auth.users, or insert the profile after a session exists. Compare auth.uid() to the column that stores the user id. Do not drop row level security.
5. Leave signInWithPassword, signInWithOtp, and unrelated pages alone unless this button is calling the wrong method.

Say which file you changed.

How to keep the next edit from doing this

  1. After any prompt that touches the signup form, the auth client, environment names, or SQL, use a private window on the published URL. Click once with the Network panel open and an address you have not used here. You want a request, a visible result, and a row under Authentication, then Users.
  2. In that diff, search for onSubmit, onClick, type="button", signUp, createClient, catch, toast, profiles, NEXT_PUBLIC_, and VITE_. Tell the editor not to replace the handler with a placeholder, not to drop the error text, and not to rename those environment names.
  3. Keep an auth rewrite out of an unrelated feature. If the next click is dead, restore the last good publish and reapply the visual change on its own.

The free scan only loads the page. It does not click Sign up. To rerun the on-site steps, 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 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, Lovable, Bolt, v0, Cursor, or Replit.