Authentication bugs are good at pretending to be something else.
A button does nothing, so I suspect React.
A user signs in but gets sent back to the login page, so I suspect routing.
A dashboard loads but returns no records, so I suspect the query.
Sometimes the frontend really is wrong. But authentication sits across several layers at once: the browser, the auth provider, the session, routing, database authorization and sometimes email links. A problem in any one of them can appear on the screen as the same simple symptom: “it doesn't work.”
The way I debug these problems now is to stop asking, “What is wrong with the login page?” and start asking, “At which layer did the expected state stop being true?”
First: did the authentication request succeed?
I begin with the request itself.
If I am using Supabase password authentication, I want to inspect the returned data and error, not just assume the click handler ran correctly.
Conceptually:
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
console.error(error)
return
}
For a production interface I would not dump sensitive information into logs forever, but during diagnosis I need to know whether the auth provider accepted the credentials.
This separates two very different bugs: the authentication call failed, or authentication succeeded but my application reacted incorrectly.
Without that distinction I can spend an hour changing JSX around a backend error.
Second: does a real session exist?
A successful-looking interface is not proof of a session.
Maybe I set local React state such as isLoggedIn when the form completed. If that state disappears after refresh, the application may suddenly think the user is logged out.
The authentication client should be the source of truth for the session.
The principle matters more than the exact method name:
Do not invent authentication state separately from the system that owns authentication.
If the auth system says there is no valid session, a React boolean cannot make the database believe otherwise.
Third: is my application rendering before auth finishes loading?
This produces one of the most confusing visual bugs.
The app starts. React initially has no user because the session check has not completed. A protected-route component sees user === null and immediately sends the visitor to /login.
A fraction of a second later, the session is restored—but the user has already been redirected.
From the user's point of view, “the site keeps logging me out.”
The fix is often an explicit loading state.
I want three states, not two:
auth loading
authenticated
unauthenticated
While authentication is still being resolved, I show a loading state instead of deciding that the user is signed out.
That tiny distinction removes a surprising number of redirect loops.
Fourth: check the redirect URL
Email confirmation, password recovery and OAuth introduce another layer: external redirects.
The application may be correct locally but fail in production because the auth service is still sending the user to http://localhost:5173, or because the deployed URL is not included in the provider's allowed redirect settings.
Whenever authentication behaves differently after deployment, I check the configured Site URL, allowed redirect URLs, protocol, www versus non-www, and the route handling the callback.
A one-character URL difference can look like a major application bug.
Fifth: login is not authorization
This is the part I wish more beginner tutorials emphasized.
Authentication answers:
Who is this user?
Authorization answers:
What is this user allowed to do?
A user can log in successfully and still receive an empty result from the database because Row Level Security correctly blocks the requested rows.
That does not mean the frontend query is broken.
Suppose I query:
const { data, error } = await supabase
.from('projects')
.select('*')
If RLS is enabled and there is no matching SELECT policy, the result may not be what I expected.
The right debugging question becomes: what policy should allow this authenticated user to read these rows?
I never solve this by disabling RLS permanently just to make the frontend work. That removes the security boundary instead of fixing the policy.
Sixth: verify the IDs you are comparing
Another bug looks exactly like an RLS bug: mismatched ownership data.
Maybe the authenticated user's ID is one value, but the record was created with a different profile ID, an email address or null.
Now the policy is doing exactly what I told it to do. The data model is the problem.
I inspect one failing row.
What value is stored as owner? What value exists in the current authenticated user? Are they the same type? Was the record created before the ownership logic existed?
Debugging becomes easier when I stop staring at abstractions and inspect one real example.
Seventh: look for stale assumptions after sign-out
Signing out should clear more than the visible user name.
If the app uses cached profile data, global state or local storage, old data can remain after the session ends.
Then another user signs in and sees part of the previous user's interface until new data loads.
That is both confusing and, depending on the data, potentially serious.
On an auth transition I think about user state, cached requests, profile state, private UI and pending actions.
Authorization at the backend still protects real data if implemented correctly, but the frontend should also reset cleanly.
Eighth: do not confuse CORS with authentication
Sometimes the browser refuses a request before auth logic even matters.
A network error, blocked origin or incorrectly configured server endpoint can appear as “login failed.”
I open the Network tab and inspect the actual request.
Did it leave the browser? What status code came back? Was there a preflight request? Did the server reject the origin? Did the response contain an auth error or a networking error?
The console message normally gives a better direction than the button on the page.
Ninth: reproduce with a second account
When a system has user-owned data, I test with two ordinary accounts.
This quickly exposes assumptions that one developer account hides.
I check whether User A can see only what A should see, User B can see only what B should see, sign-out truly changes identity, switching accounts does not retain private state, and policies behave consistently.
A system that works only with the first account created is not finished.
My debugging order
When auth breaks, this is roughly the order I follow now:
- Did the request succeed?
- Is there a valid session?
- Did the application wait for auth initialization?
- Is the redirect URL correct?
- Is the user authorized for the requested data?
- Does the row ownership data match the authenticated identity?
- Is old client state interfering?
- What does the browser Network tab actually show?
- Can I reproduce it with another account?
That order saves me from random edits.
Authentication bugs feel chaotic because several systems meet at one screen. The solution is to separate them again.
React owns the interface.
The auth provider owns identity and sessions.
The router owns navigation.
The database policies own data authorization.
My job while debugging is to find which layer first stopped matching the assumption made by the layer above it.
Once I find that boundary, the “frontend bug” usually becomes a much smaller and more understandable problem.
Technical reference
- Supabase Auth with React: https://supabase.com/docs/guides/auth/quickstarts/react
