last week, after two and a half years, i wrapped up my time at Clerk
i still remember what made me join: the product, the obsession with detail, and a team of superstars. but i already told you that, and for the technical side of things there's a talk i gave a few months back (though it doesn't make much sense without the narration, my bad!). this one comes from a different place. it's a reflection, a few notes, and some takeaways i wanted to share
in early 2024, i'd just built HTNX, a half-joke experiment that basically returned React elements from Server Actions. it worked, it was fun at the time, and i couldn't stop thinking about how React and Next.js handled all my crazy requests. send some text? sure. send a component? sure. send a... whole new page? why not!
HTNX demo
after sleeping on it for a few days, i decided to do something about it. opening the React codebase on one screen, reading it file by file, while on the other screen trying to piece together something that bridged all the gaps, sounded like a reasonable approach. should be done in a weekend, i thought
that "weekend" took absolutely all my free time for the next few months. a lot of failed attempts, and a fair amount of bothering Josh Story and Dan Abramov, before anything really worked
the result of all that was R19, and from it two posts, one on the backstory and the other on the code. Lee shared the first one, both later got featured in This Week in React, and eventually they found their way to Clerk
R19 post
when they reached out about interviewing, i jumped right in. we talked data fetching, caching, performance, and scaling systems, and for that i had two fresh months of wrong turns to back up why i'd pick one approach over another. i joined later that same week
i'd used the product before, and it was way ahead of any of the alternatives back then. once in, the pace was nothing like the bigger companies i'd worked at, in the best way. during that time i got to work on something i admired, with a bunch of people i looked up to
two and a half years, mostly spent on one thing
i knew what i was signing up for. i joined Clerk to work on moving its dashboard, which back then was more than 500,000 lines of code, from Next.js's Pages Router to the App Router. that wasn't the only goal, of course, but to keep things simple, let's say it was. and we had a good reason for it: our customers were using Clerk with the App Router. if they were going to hit blockers or pain points, we wanted to hit them first
my most recent experience with what was then the bleeding edge of the web was from something i built from scratch and could break (and it did), hit a dead end with, try again, and break again (which it also did). now, the dashboard was where thousands of people managed authentication for their apps, every single day. it had to keep working while we swapped the foundation underneath it, in a new codebase, while features kept landing in the old one
the old app had grown with the company, and so had the ways of doing the same thing. our data fetching wrapped SWR in helpers for paths, fetches, hooks, and types, and we still overfetched and underfetched. the migration was a chance to ask what each page actually needed, and where that work should happen. we moved to React Query, with server rendering and streaming (barely out at the time, and brought to the lib by @ephem, who was working on this project too)
and that's where i learned a much-needed lesson. the wrappers were nice. they looked good, clever, something to be proud of right after you finish writing them. but if someone reading the codebase can't look at a data fetching function and see the path it's fetching, the body it's sending, or the key it's caching under, it will become a liability. keeping things readable ended up being one of the most important parts of making room for a growing product and team
even though this may sound outdated in the AI era, i still firmly believe it. at the very least, it saves context
the prototype repository went from one commit to piles of them, and we still hadn't answered a fairly important question: how do we launch this?
our manager on that project taught me a lot about managing expectations: what we could deliver, what people inside the company expected, and what users would actually get while the bigger project was still going. there's a lot of politics in doing something long-term with no visible results
but at some point, we needed to start shipping what was ready and keep building the rest. a finished page shouldn't wait for the whole dashboard, and an unfinished one shouldn't go public just because it lived in the new app
Next.js already let app and pages live side by side. we still needed both versions of the same page available, and a way to decide who saw which. so we put the App Router routes under a /beta prefix. middleware could take a normal dashboard URL and rewrite it to the matching /beta route, without changing the address the user saw. each route had one of three statuses: beta for unfinished routes staff could opt into, rc for broader company QA, and live for everyone. the check looked something like this:
const shouldRewriteToAppRouter =
firstRouteMatch &&
(firstRouteMatch.status === "live" ||
(firstRouteMatch.status === "rc" && isStaffMode) ||
(firstRouteMatch.status === "beta" && isAppRouterMode));staff mode needed a staff identity and a cookie, and App Router mode also needed an opt-in. that meant we could deploy code without customers seeing it yet. one page live, another in QA, another still being built, all in the same deployment
one morning in early 2025, someone posted a recording of the dashboard, along with a comment, and it was rough. the kind of clip that's hard to argue with: you're trying to use a product, and it goes through a few different loading states, with fonts and things that definitely shouldn't be moving around... moving around
dashboard recording
the replies had their own ideas. "just use htmx bro," they said
header skeletons didn't match what loaded in. a banner shifted the layout. fonts flickered between routers. navigation and animations didn't behave the same way twice
we had the first fixes out within an hour, and most of the layout shift was gone. fonts, navigation, and the speculation rules came soon after. a few of us worked through it together
it was stressful at the time. there was so much going on under the hood, if only people knew... and you know what? no one cares, and that's how it should be. users shouldn't EVER need to worry about how an app works underneath, or why. that day, after fixing the most pressing issues, we came out much closer as a team. the turnaround was quick and effective, and it proved our ideas about code reliability and scalability right
rebuilding a dashboard gives everyone a chance to propose a new pattern. then another one. then a slightly more clever version of that one. with enough people, you can spend a very long time rebuilding the mess you started with
so we narrowed the scope and stuck to what we'd decided, even when a new idea was tempting. first a dashboard that felt like one product from page to page, then we could keep going back and making it better. and to get people on board, it helped to take away something they already hated doing before asking them to change how they worked
a lot of how i think about this came from working with Max, Emil, and Mery. in Max's PRs, how he got to a solution was often as interesting as the solution. Emil's attention to animations and interactions changed what i notice. and Mery could take an idea somewhere i didn't expect, and was always kind while i figured it out (she also helped with early prototypes for this site :))
i don't think i've seen a dashboard more beautiful than Clerk's. i'm biased, sure. but you can move from any page to another and it still feels like the same product. after dealing with the opposite for so long, that makes me very happy. coherence, that's key
the migration took more than a year. long enough for the code we wrote at the start to need fixing too. eventually, the work started looking like this:

by the end, i'd never been happier with a codebase. i knew how it worked, and i'd been there for most of the decisions behind it. the patterns were clear and documented, and everyone knew how to add something without making the next person's job harder
the migration got a proper goodbye: a week in Lanzarote with almost everyone who worked on it. a Spanish island off the coast of Africa that looks a lot like Mars, the right kind of far away to finally call it done
photo 1 of 4
photo 2 of 4
photo 3 of 4
photo 4 of 4a few things i keep coming back to, now that it's over
one of my favorite memories from this time is from Amsterdam. we were by the canals, the usual chaos unfolding all around us, talking about a bug we'd traced through our dashboard, into Next.js, and all the way down to a core React feature. a whole city to look at, and we were talking about a React bug that could sometimes, possibly, land you on an uncaught error page that looked ugly and that we had no control over. i had a great time
photo 1 of 3
photo 2 of 3
photo 3 of 3believe it or not, the way we did this migration is almost unthinkable these days. we had one file on one screen and an editor on the other. we read it, (tried) to understand it, dug deep, and came out with a new version. now repeat that for hundreds of pages. then a pattern stops working or doesn't scale, so you fix it, change everything, and move on to the next
i say it's almost unthinkable these days not because of any blunders we made, but because of how artisanal that work feels, even to talk about. and i'm grateful i did it, pushing through a challenge like that for years until everything felt right. there was a level of commitment and knowledge that's hard to replicate when you're just supervising someone else's work. do you need that? do you want that? idk, but it was a really nice feeling
over time, as ai became part of everyone's day-to-day at the company, we started facing new problems. not with code reliability (we always had a strong review culture), but with scale: spreading the right patterns and keeping the bad ones from spreading. there's a lot of work in figuring out how to talk to the machines, to the agents. we'd done a great job documenting everything for humans, but now we needed agents to understand it too, and not lose it in their constant fight for context
i'm glad i got to know a codebase like that, the painful way, figuring it out with other people. i'm not sure i'll get that again. a lot of that work would look different with today's tools, but i can't separate what i learned from the time we spent on it together
there were things i really wanted to see through, and i'm leaving super proud of what we shipped. so, to everyone i worked with: thank you. i learned a lot from you and had so much fun doing it 💜
photo 1 of 2
photo 2 of 2as for me, i'm taking a couple of months off to travel and work on a few creative things i've been wanting to do for a while, and (hopefully) write about it too
oh, and i'll be back with more info about where i'm going next... well, soon, let's say :))
latest commit : 5 additions 5 deletions