Virtual Internships · 2026
Signup Funnel Rebuild
Was 3+now 1
Signup was three screens, one of which collected nothing. The rebuild deleted a page, collapsed the rest into a single transaction, and then had to deal with everyone who was already partway through the old flow when it shipped.
Three screens, one of them an animation
Signup was three screens. basicDetails collected first name, last name, date of birth, timezone, current country, and how you heard about us. personalizationScreen followed. Then congratulationsScreen: 148 lines of TSX, 160 of SCSS, one Lottie file, collecting nothing.
Why the registration screen went
It existed to gather timezone and a handful of other fields. It also stood between signing up and seeing what the platform actually offered — one more screen before anyone could tell whether the thing was worth their time. The reasoning for removing it was that the length of signup was where people were being lost.
The fields didn’t need a screen of their own. Country and timezone are now inferred from IP through a useIpBasedDefaults hook, because the browser already knows roughly where you are and asking is friction. The rest moved into the application flow, where someone is already filling in a form and one more field costs nothing. congratulationsScreen was deleted outright.
One call
POST /register/start-onboarding replaced the walk. Inside it, one database transaction creates the Intern, the InternBatchPartnerMapping and the InternApplication together. batch_id is required up front for the flows that need it, rather than discovered missing three steps later.
I wrote both sides: the frontend deletion and the endpoint behind it.
The people already halfway through
That was the hard part. At deploy time someone could be sitting between old screen two and three, holding a partly-built record, with no way to finish a flow that no longer existed.
So the endpoint treats an existing intern as the normal case rather than the exception. If the record is there it updates in place and carries the name fields forward instead of minting a new uuid. Before creating the batch mapping it looks for one, and skips both the mapping and the application if it finds it. If is_intern_onboarding_started is already set it returns a token and touches nothing at all.
One step isn’t idempotency. The old flow wrote a UserPartnerBatchMapping keyed by uuid, before the user was an intern. The new one writes an InternBatchPartnerMapping keyed by intern_id. The transaction creates the second and deletes the first, so a record that straddled the pre- and post-authentication boundary lands cleanly on one side of it.
Every branch in that method is a version of the same question: what if they’ve already done part of this?
What wasn't measured
Nothing was. There’s no before-and-after on signup completion, drop-off or time-to-first-screen — no dashboard, no instrumentation added, no number in any of the four pull requests. The change was made on the reasoning above, not on a measurement, and I’m not going to claim a result I didn’t record.
Related
The batch-selection frontend was rewritten separately — hooks, an RTK Query migration, and a bug that had been dropping query params on every request. That one has its own write-up.