Auth: Category First, Tool Last

Case study - Auth; Same six questions, two opposite tools.

Auth is done in many ways, most of them wrong. You'll hear advice of all sorts: never build your own, never outsource, own the data, own the users, etc. etc. Implementations are almost commoditized, and there's a few big players that absolutely dominate the market.

Auth0 alone is worth naming: Okta bought it in 2021 for $6.5B, and the combined company now sits behind 18,000+ customers' login pages. Pair that with Cognito, Firebase Auth, and Azure AD B2C, and you're picking a login screen operated by a company several magnitudes bigger than the startup renting it. Pricing runs on monthly active users - free for a while, then a step function the moment you cross the line. Oh, and the moment you mention B2B, the numbers triple.

I think they have their place, and I'll recommend them as much as the next person, but this case study is about two implementations which went elsewhere. In two very different directions, actually.

One was an NGO: small, straightforward, selective. Imperative that they own their users data, and that they do it on the cheaper side. The login requirements were light (usual suspects): website and mobile apps. They didn't need any fancy bells and whistles. No vendors as such, no "fancy" features. Simple, default even, roles in the feature map.

My product roadmap was clear: deliver the API, onboard a small team, done. The call was Django. Auth was Django AllAuth. Configured in a day, tested, shipped. Still running.

It wasn't friction-free the whole way. AllAuth's documentation thins out once a deployment goes fully headless - no browsable session to fall back on and debug against, API-only end to end. Debugging cost hours an API-native provider wouldn't have charged for. Not a wall. A speed bump, paid for in time instead of a subscription.

The instinct here was: avoid the urge for complexity. The "right" auth setup is not the one with the most features - it's the one that solves the actual requirements without adding exposure you don't need. AllAuth wasn't limiting for this product, because this product was not intending on going near the limit.

The other was a startup. Software as a service. Active users, corporate users on the roadmap, multi-tenant architecture, and specific product features planned with auth implications:

These aren't theoretical "room to grow" - they're expected features. And each one would struggle fitting into an auth library out of the box. Lots of custom work, lots of exposure.

Impersonation is the clearest example. Secure impersonation is time-limited, auditable, and scoped - the impersonating admin's permissions apply as a restriction, not the target user's. That's not in library scope. AllAuth, for instance, ships zero impersonation - you'd bolt on django-hijack or django-loginas, a second dependency with its own trust boundary to audit. Libraries give you OAuth flows; delegation isn't designed in. You implement that yourself, or you inherit someone else's implementation of it.

The Process

Before going into technical details, before even looking at programming language best practices... I look at the product:

  1. What logins does the product need now, what's in twelve months?
  2. Does the product have user layers and isolation? Are users scoped to organizations/companies/environments?
  3. How will admins work with the end user? If there's no separate access/app - is impersonation the way?
  4. RBAC? Access on feature-level or just access/no-access?
  5. Are there "types" of accounts? APIs or service-to-service requirements?
  6. Are there self-hosting and cost constraints?

For the NGO: 1 and 6, nothing else. AllAuth. For the SaaS: all six. Identity provider.

The tool follows from the requirements. Once the category is clear, the specific tool choice is about painting between the lines: pricing, ops overhead, API quality, community, migration.

Why Zitadel specifically, for this product:

The Execution

Proposing Zitadel at a startup means proposing new ops, a migration, and a learning curve. The engineering argument ("we need impersonation in 3-4 sprints") is not the same as the business argument ("migrating later costs more than starting right").

The business argument:

Getting the signature is being explicit about what does and doesn't go, not relying on "self-evident" arguments. Explicitly naming requirements is explicitly bringing clarity - always good, very often crucial.

The existing system had hand-rolled Google OAuth. Hacked together, essentially. Users were embedded in the app database, not in an identity provider. The usual mess followed that choice, with casual references, unstructured access patterns, and generally taking user accounts lightly. Migrating away from that brought compounding returns for future certification.

Accounts were replaced with references, and references remapped to new logins through email. The whole mapping ran against a staging copy first - migration script, edge cases, and all - before it touched a single production account. The user had to log back in once, before continuing as expected. We've purged some needless profile fields and fluff that wasn't actually used in a meaningful manner - streamlining to reduce moving parts. Separation of concerns doing the work.

Same honesty applies on this side. Zitadel had its own rough edges - email delivery and deployment needed real iteration, a few days spent tuning validation-email flows that a hosted provider would have handled invisibly. Not heavy engineering, more like decrypting someone else's spec and docs instead of owning the flow outright. That's the actual tradeoff for self-hosting and control: you inherit the plumbing along with the flexibility.

Even with that friction, the auth layer landed inside phase one, sprints ahead of the original estimate - the org and invite model was already live for pilot users before the harder testing work was supposed to start. The instinct to "pick the right auth tool" is correct. The common mistake is treating it as a purely technical problem - comparing libraries, reading benchmarks, asking Discord/Twitter.

Get the requirements clear. Find the category. The category then reduces it to two or three choices. The further constraints (ops, pricing, migration) resolve to the specific one. Follow the order.

And sometimes the requirements say: just use a lib. Don't need to fight it.

If your auth layer needs the same kind of call - let's talk →

Book a discovery call