james@perkins :~/writing$
← writing / thought leadership thoughts

Why & How We Built a Better Auth Abstraction Layer

Open source projects live and die by friction. So we built auth abstraction layer to make it easier.

Authentication is a hurdle every application has to tackle. Whether you choose a polished SaaS provider, an open source project, or roll your own solution, the decision isn’t easy and the right pick can make or break your contributor experience and future scalability.

I want to take you through our journey with Unkey: how we started with Clerk, why that was great (and where it fell short), and exactly how we transitioned to our own custom authentication abstraction layer that powers both smooth self-hosting and amazing developer experience.

Why Authentication Choice Matters

Every app no matter if it’s a tiny hobby project or a massive SaaS platform needs authentication. The options are endless and boil down to three routes:

Each of these comes with tradeoffs. SaaS is quick and enterprise-ready, but sometimes comes with lock-in or complicated local dev. Open source gives you control and self-hosting, but means more hands-on maintenance. And building your own… Well, that’s opening a whole can of worms.

In my experience, for 99.99% of projects, picking a SaaS like Clerk is the fastest way to get started, but when you’re building an open source project where contributing should be easy, things get tricky fast.

Our Experience Starting with Clerk

When we kicked off Unkey, Clerk made getting started super easy. We had:

It was honestly great when you’re shipping fast and want to focus on actual product features.

Where Clerk Fell Short for Open Source Contributors

Open source projects live and die by how easy it is for others to contribute. Using Clerk behind the scenes meant every contributor had to:

No matter how much we documented it, spinning up a local dev environment became way too much friction. Contributors working on even the tiniest bug had to spend time registering with Clerk, setting up OAuth providers, and configuring secrets.

This was not sustainable.

There were way too many DM’s and issue comments like:

“I just want to fix a typo, why do I have to register for Clerk first?”

Instead of focusing on features, we were writing docs and answering setup questions. That’s not the vibe for an open source project.

Figuring Out What We Actually Needed

It was time to go back to the drawing board. Around April 2024, we had a big offsite and listed out EXACTLY what we needed for Unkey’s auth system for both maintainers and contributors.

Core Requirements

For Production

For Contributors & Local Dev

For Us (the Maintainers)

And possibly most important:

“We wanted contributing to feel like ‘clone, run, and go’… not ‘sign up for 3 SaaS products and mess with secrets for an hour’.”

Designing the Auth Abstraction Layer

To solve all this, we needed an auth abstraction layer that could:

This meant some big architectural changes.

We had to:

  1. Rip apart the old, intertwined code (where business logic and auth were bound together)
  2. Design interfaces and “providers” for plug-and-play swapping
  3. Build specialized tools for local auth something that required no external dependencies, worked offline, and just “magically” signed users in

A Deep Dive: How the Auth Layer Works

The Big Picture

At a high level, our new system is:

Typical Flow (With Real Provider):

  1. User submits email to sign in
  2. Our base auth abstraction passes this to, say, WorkOS
  3. WorkOS handles the magic link/code flow
  4. User submits the code
  5. We verify the code via WorkOS
  6. If successful, we set a cookie/session

Local Flow (Dev In-Memory Auth):

  1. Dev launches the dashboard
  2. System auto-signs them in as a stub user (no auth UI, no code input)
  3. A dummy cookie gets set that’s always “valid”
  4. From here, all auth checks short-circuit to “yes, user is signed in”

“In local mode, this is automatically done for the user. As soon as they launch the dashboard, it just signs them in… They never see the sign in page.”

Organization Magic

Because Unkey is all about teams/workspaces, the abstraction layer also ensures:

Technical Challenges (And How We Crashed Into Them)

Of course, this wasn’t just flipping a few switches. Here are the main technical battles—and what we ended up building to fight them.

If you’ve ever tried working with cookies in Next.js, you know it’s… let’s call it “quirky”.

“Ensuring consistent cookie access and management required really careful planning… [and] a lot of debugging.”

2. Session Handling and Caching

We needed fast user lookup but not too fast. If we cached session info too aggressively, users got stuck with out-of-date data. If we didn’t cache enough, we hammered our backends with unnecessary reads.

Several iterations later, we ditched some built-in Next.js utilities and switched to tools like tRPC to get better control over session caching and invalidation.

Lessons Learned:

No more duplicate calls. Happy backend.

4. Middleware and Local/Provider Parity

Our middleware ensures that only authenticated sessions can load protected routes (like the dashboard). We needed it to work the same way, whether the app was running locally (in fake auth mode) or in production (with a real SaaS provider).

It took a lot of tweaking to make middleware context and cookies “just work” across these different setups.

5. Decoupling Business Logic from Auth

Originally, a lot of core features were entangled with the specifics of the Clerk API. Team invites, workspace management, user settings everything called right into Clerk.

We spent weeks refactoring:

Now, we can swap in any provider (or none at all!) by swapping code behind the interface not touching feature code.

6. Real-World Bugs We Shipped (and Fixed)

Let’s be honest we shipped some bugs along the way.

Token Refresh (or, “Why Am I Getting Logged Out Every 5 Minutes?”)

Most OAuth providers give you a short-lived access token and a longer “refresh token.” If you don’t handle token refreshing, users get logged out as soon as the access token expires.

We only discovered this after shipping to production, when users started complaining:

“I just got kicked out of the dashboard after 5 minutes… Now I’m stuck on a loading spinner.”

Both Google and GitHub OAuth required us to implement full token refresh cycles, and to ensure the session was seamlessly refreshed in the background.

Once we fixed that (and combined it with auth request deduplication), sessions became smooth and reliable again.

What We Learned and Where We’re Going

Transitioning away from Clerk (and away from any direct provider lock-in) was a massive refactor but it was worth every hour:

A Last Word to Open Source Maintainers

“No matter how we documented it, there was no easy way to direct people through the Clerk setup. So we decided to fix this problem at the core.”

If you’re running an open source project, prioritize easy contribution. Authentication shouldn’t stand in the way of new maintainers, bug fixers, or your own ability to keep shipping.

0 claps
JP
james perkins
CEO & co-founder at Unkey. Writing about the messy middle between a blank editor and a working company.

Discussion // 0 comments

sort: oldest ↓
?
be excellent to each other
no comments yet — be the first.