Stephen

Hackett-Delaney

Full Stack Software Engineer

ArticlesDebugging

Auth Debugging Layer by Layer

debuggingauthenticationreact-nativesupabaseexpo

February 16, 2026

The Supabase auth logs showed a 200 response. The user had the correct role in their metadata. No error was logged on the client. But after logging in, the app sat on the login screen and did nothing.

This one took us through four layers before we found the real problem. Each layer looked correct in isolation. The bug was in the gap between them.

How Did We Get Here?

We'd been debugging our Maura React Native app's login flow for a while at this point. We'd already dealt with stale CSV data, a database reset script, AsyncStorage deletion, and simulator keychain issues.

The development cycle was: reset the brand, activate the invite via web, log in on the simulator. After clearing various caches and fixing various issues, we tried logging in again. The form submitted. No error appeared. But the app stayed on the login screen.

Layer 1: The UI

Symptom: Tap login, nothing happens. No error message, no navigation.

Initial assumption: The login is failing silently.

Investigation: Added console.log("data", data) before the signInWithPassword call:

const onSubmit = async (data: any) => {
  console.log("data", data);
  const { data: { user }, error } = await supabase.auth.signInWithPassword({
    email: data.email,
    password: data.password,
  });

Result: The data log appeared in Metro. The form was firing. But no "Login error:" log appeared afterward — which meant no error was returned. The login appeared to succeed.

Verdict: UI is working. The form submits. Move deeper.

Layer 2: The Auth Response

Symptom: Login fires, no error logged, but the app doesn't navigate.

Initial assumption: Maybe signInWithPassword returns a user but the session isn't being stored.

Investigation: Added detailed result logging:

try {
  console.log("calling signInWithPassword...");
  const result = await supabase.auth.signInWithPassword({
    email: data.email,
    password: data.password,
  });
  console.log("login result", JSON.stringify({
    user_metadata: result.data?.user?.user_metadata,
    session: !!result.data?.session,
    error: result.error?.message,
  }));
} catch (e) {
  console.error("signInWithPassword threw:", e);
}

Result: The "calling signInWithPassword..." log appeared. But the "login result" log never appeared. Instead:

ERROR  signInWithPassword threw: [Error: Failed to write value.
"The folder 'e6e9721590ffc7d457e55c3e577989d2' doesn't exist."]

The call wasn't returning — it was throwing. The original code didn't have a try/catch around the await, so the error was swallowed by the async function. No error was logged because the code that logs errors (checking if (error)) never ran.

Verdict: Auth call succeeds on the server, but the client-side SDK throws when trying to persist the session. Move deeper.

Layer 3: The Session Provider

Symptom: Even before login, role was logging as undefined.

Investigation: Added logging to the onAuthStateChange callback in the SessionProvider:

const onAuthStateChange = useCallback(
  async (session: Session | null) => {
    setSession(session);
    console.log(
      "onAuthStateChange user_metadata:",
      JSON.stringify(session?.user?.user_metadata)
    );
    setRole(session?.user.user_metadata.role as Role | null);

Result: On app load (before any login attempt):

LOG  onAuthStateChange user_metadata: undefined
LOG  role in app index useEffect undefined

No session was being restored on startup. This made sense — if the previous session couldn't be written to storage, there's nothing to restore.

But we also checked the server directly:

curl -s -H "Authorization: Bearer <service-role-key>" \
  "https://<project>.supabase.co/auth/v1/admin/users/<user-id>"

The auth user had user_metadata.role: "brand_employee" correctly set. The data was right on the server. The client just couldn't access it because the session was never persisted locally.

Verdict: Server is correct. Client can't persist. Move deeper.

Layer 4: The Storage Layer

Symptom: AsyncStorage throws "folder doesn't exist" on write.

Root cause: Earlier in the debugging session, we'd run rm -rf on the RCTAsyncLocalStorage_V1 directory to clear a stale session. This deleted the directory itself, not just its contents. AsyncStorage's native iOS module expects the directory to exist and doesn't recreate it.

The fix:

mkdir -p ".../RCTAsyncLocalStorage_V1"

One command. The directory existed again. The next login persisted the session, the onAuthStateChange callback fired with the user's metadata, role was set to "brand_employee", and the app navigated to the brand dashboard.

The Full Stack of Assumptions

Here's every assumption that had to be wrong for this to present as "login doesn't work":

LayerAssumptionReality
UI"Login is failing silently"Login fires correctly
Auth"signInWithPassword returns an error"It throws, which the code didn't catch
Session"user_metadata.role is missing"It's set correctly on the server
Storage"AsyncStorage works"The directory was deleted

Each layer's assumption pointed at the layer above or beside it. The actual cause was at the bottom — a filesystem issue from a previous debugging step.

The Process Shortcomings

We didn't have try/catch around the auth call. The original code used destructuring on the return value of signInWithPassword. If the call throws (rather than returning an error object), the destructuring never runs and no error is logged. The failure is completely silent.

We didn't log enough at each layer. The SessionProvider had no logging. The login handler had minimal logging. We added instrumentation reactively — one layer at a time — instead of having it from the start.

We didn't connect the cache clearing to the auth failure. The AsyncStorage deletion and the login debugging were separated by time and by other issues (keychain, role checking). We didn't think to ask: "Did that thing we did 30 minutes ago break this thing we're doing now?"

Our development debugging was destructive without being tracked. We ran rm -rf commands, keychain resets, and database mutations during a debugging session without keeping a log of what we'd changed. When the auth broke, we had to re-derive what had changed in the environment.

The Takeaway Pattern

When debugging auth, work from the outside in:

1. UI Layer:      Does the form submit? → console.log the form data
2. Network Layer: Does the API call succeed? → try/catch + log the full result
3. Session Layer: Is the session persisted? → log onAuthStateChange
4. Storage Layer: Can the client write? → check the storage mechanism

At each layer, log what you find before moving deeper. The first layer that shows unexpected behavior is where the bug lives — or at least where it manifests. The cause might be deeper still.

What I Learned

Errors that are thrown are different from errors that are returned. Supabase's signInWithPassword can fail in two ways: returning { error } in the result object, or throwing an exception when the underlying storage fails. If you only check for the first, you'll never see the second.

Debugging is rarely about the thing you think it is. We started with "login doesn't navigate" and ended at "a directory doesn't exist on disk." Four layers. Each one looked correct until you looked at the one below it.

Keep a log of what you change during debugging. Every rm -rf, every config edit, every script run is a potential cause of the next bug. When you're deep in a debugging session, it's easy to forget what you changed an hour ago.

The throughline of this entire series is the same lesson: the problem is always one layer deeper than where you're looking. NULL values hide behind query filters. FK constraints hide behind delete statements. Missing directories hide behind auth errors. The only way to find them is to keep peeling back layers until the real cause is exposed.


This is Part 5 of the Debugging series. The full series: Part 1: When NULL Breaks Everything | Part 2: Building Safe Reset Scripts | Part 3: The Subtle Art of Clearing Caches | Part 4: iOS Simulator Gotchas

© 2026. All rights reserved