← Writing · Essay · 5 min · 20 February 2026

Debugging the previous engineer’s mental model.

When you inherit a broken migration, you are not debugging the system. You are debugging the assumptions the previous engineer had about it. A specific account from restoring 1,643 locked-out users.

I picked up a broken AWS-to-Supabase migration in November 2025. Roughly 1,640 users were locked out of the new system. Magic links did not work. Password resets did not work. The previous engineer had completed the data movement, declared the migration done, and left.

The bug was not in the project code.

It was in the Supabase GoTrue auth service. Three separate root causes, all interacting. None of them obvious from the project repo. The previous engineer had presumably tried things, gotten nowhere, and concluded the system was correct.

What I was actually debugging

On paper, I was debugging GoTrue. Within an hour, I realized I was debugging the previous engineer’s mental model of GoTrue.

He had assumed certain users existed in auth.users because they existed in the application’s user table. They did not. He had assumed that creating a row in auth.identities would unlock magic links. It would not, not without the corresponding auth.users row in the right shape. He had assumed that the bcrypt hashes from AWS Cognito would import into GoTrue’s password format as-is. They would not.

Every assumption shaped how the migration scripts were written. Every wrong assumption left a slightly different shape of broken user. That is why there were three separate root causes blocking the same 1,640 users.

How I unblocked it

The trick was separating “what the data says” from “what the previous code assumed about the data.”

I started with the canonical user set. Mailchimp had 1,650 subscribers. AWS had a 2,144-user export. Live Supabase had a partial subset. None of these were the same list. Before touching GoTrue, I built a single source of truth that said which user was in which group.

Then I reproduced each failure mode locally. Took one user from each broken state. Walked their record through GoTrue. Watched it fail. Each failure pointed at a different missing piece. Three failures, three fixes.

The fix itself was four idempotent SQL steps. Anyone could run them. Anyone could re-run step three without re-running step one. The previous engineer’s scripts were not idempotent, which was part of why he could not isolate the problems. Each test run mutated state.

1,643 users restored. Zero data loss. Forty-one billable hours through Arc.dev.

The pattern

When you inherit a broken migration, you are not debugging the system. You are debugging the assumptions the previous engineer had about the system.

Sometimes those assumptions are written down. Usually they are not. Sometimes the previous engineer is available and you can ask. Usually they are not. The work to find the assumptions is most of the work.

A working method

Three habits I now use on inherited migrations:

Map the data before you touch the code. What is the canonical set? What are the overlapping sources? Which one is the source of truth? Write that down. The previous engineer probably did not.

Reproduce each failure mode against one row. Not against the whole dataset. One user from each broken state. Walk that row through the system until it fails. Read the actual logs. The shape of each failure points at a different assumption.

Write idempotent migrations. Numbered steps. Each step re-runnable in isolation. If you cannot rerun step three without re-running step one, you will never isolate the problem, because every diagnostic attempt mutates state.

The harder lesson

The hardest part of inheriting a broken migration is not technical. It is resisting the urge to call the previous engineer an idiot.

He was not an idiot. He had a model of how Supabase worked. The model was wrong in three specific places. Those three places happened to all land on the same users. From his vantage point, he probably saw an inconsistent bug that would not reproduce. That is what a wrong assumption looks like from inside.

When you arrive, you have an advantage he did not: a fully broken system. The data has stopped lying. Every locked-out user is a clear failure mode you can isolate.

Use that.

Filed under Debugging · Auth · Supabase

Inherited a broken migration?

If you are looking at a stalled AWS-to-Supabase or Auth0-to-Supabase move with users locked out, this is the shape of work I do most often.