Why & How We Built a Better Auth Abstraction Layer

8 min read
By:
James Perkins James Perkins

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:

  • Software as a Service (SaaS) providers (like Clerk, Auth0, etc.)

  • Open Source Projects (Lucia, Auth.js, etc.)

  • Roll-your-own homebrewed auth system

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:

  • an authentication layer up in minutes

  • analytical data at our fingertips

  • organizations and team management (since our core model is “workspaces”)

  • all those nice-to-have enterprise auth features

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:

  • Sign up for their own Clerk instance

  • Enable organizations

  • Configure Google, GitHub, and Magic code

  • Add Clerk API keys and secrets

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

  • Well-supported SaaS provider (for scaling with security and compliance)

  • Enterprise features: SSO, SAML, organization management, etc.

  • Organizations as a first-class citizen (since Unkey’s “workspaces” are fundamental)

  • Scalability as we grow (not getting bottlenecked by our own auth)

For Contributors & Local Dev

  • Zero setup for developers (should be able to clone, run, and go)

  • In-memory local auth: one user, one workspace, always “signed in”

  • Works offline and is self-contained no third-party outages breaking dev

  • Consistent behavior between local and production flows

For Us (the Maintainers)

  • Provider swap at any time: no heavy code rewrites if we want to switch SaaS or go open source

  • Clear separation of auth logic from business logic

  • Easy testing

  • Minimal lock-in to any provider

  • Simple self-hosting for folks who want their own instance

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:

  • Swap backend providers (SaaS or open source) with minimal fuss

  • Seamlessly switch between a “real” production auth system and a dummy in-memory fake for local/dev/test

  • Keep a clean boundary between “auth stuff” and “business logic”

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:

  • The app makes all auth requests through our abstraction layer

  • The layer decides, based on environment, whether to talk to a real provider (like WorkOS) or run an in-memory fake

  • All business logic downstream just “trusts” the auth layer and gets consistent info

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:

  • Every “user” (even in local mode) is in an org/workspace

  • Org/team APIs always work as expected, so developers can test the full flow without additional setup

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”.

  • Server components vs client components vs API routes all have their own quirks and timing issues

  • Ensuring cookies were set, read, and updated correctly everywhere was way trickier than anticipated

  • We ended up writing custom utilities to set, get, and validate cookies no matter where the code was running

“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:

  • Always invalidate session caches after changing user/org data

  • Don’t trust “clever” Next.js helpers too much; sometimes hand-rolled is safer

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).

  • Had to handle token refreshes, expiration, and all the edge cases

  • Error-handling and redirection logic needed to be 100% parallel in local and live mode

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:

  • Pulling all references to auth provider APIs into the abstraction layer

  • Writing new interfaces so business logic ran on generic user/session/org data, not provider specifics

  • Moving all “who is logged in?” questions to ask our abstraction, not Clerk/WorkOS directly

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:

  • Contributor friction is now basically zero you can just clone, run, and hack

  • Self-hosters can choose any provider, or just use local fake auth

  • We can swap production providers any time, with no wild rewrites

  • Code is clean(er), with obvious boundaries between business and auth logic

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.