DEV Community

Cover image for Plain Text Auth Config vs Managed Services: When to DIY and When to Delegate
Alan West
Alan West

Posted on

Plain Text Auth Config vs Managed Services: When to DIY and When to Delegate

There's something deeply satisfying about a .env file. No dashboards, no vendor lock-in, no surprise pricing emails. Just keys and values, readable by humans and machines alike. Plain text configuration has been the backbone of developer workflows for decades, and honestly, it's not going anywhere.

But here's the thing — just because plain text can handle your auth configuration doesn't mean it should. I've been on both sides of this, and after migrating three projects from hand-rolled JWT setups to managed auth services last year, I have opinions.

Let's break down when plain text config is the right call, when a managed auth service makes more sense, and how the major players stack up.

The Plain Text Approach: Rolling Your Own

The classic setup looks something like this. You've got your .env file:

# .env — the OG config format
JWT_SECRET=your-super-secret-key-here
JWT_EXPIRY=3600
OAUTH_GOOGLE_CLIENT_ID=abc123.apps.googleusercontent.com
OAUTH_GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxx
SESSION_STORE=redis
SESSION_TTL=86400
Enter fullscreen mode Exit fullscreen mode

And your auth middleware, hand-rolled with love:

// middleware/auth.js — the "I'll just do it myself" approach
import jwt from 'jsonwebtoken';

export function verifyToken(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token provided' });

  try {
    // Every rotation of JWT_SECRET means redeploying
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ error: 'Invalid token' });
  }
}
Enter fullscreen mode Exit fullscreen mode

What's great about this:

  • Total control over every aspect of the auth flow
  • No external dependencies or API calls at runtime
  • Plain text config is version-controllable (minus the secrets, obviously)
  • Zero vendor lock-in
  • Free, forever

What will bite you eventually:

  • Secret rotation requires redeployment
  • You're responsible for every security patch
  • Adding OAuth providers means writing integration code for each one
  • Session management, rate limiting, MFA — all on you
  • That one weekend where you discover your token validation has a subtle bug

I ran a side project on hand-rolled auth for two years. It worked great until I needed to add Google and GitHub login. Suddenly my "simple" auth layer was 800 lines of OAuth callback handling, token refresh logic, and edge cases I hadn't thought about.

The Managed Service Approach

Managed auth services handle the heavy lifting so you can focus on your actual product. The trade-off is straightforward: you give up some control and take on a dependency, but you get battle-tested security, pre-built UI components, and OAuth integrations that just work.

Here's how the same auth flow looks with a managed service:

// middleware/auth.js — the "let someone else lose sleep over this" approach
import { authMiddleware } from '@your-auth-provider/sdk';

// That's it. That's the middleware.
export default authMiddleware({
  publicRoutes: ['/api/public', '/api/health'],
});
Enter fullscreen mode Exit fullscreen mode

Night and day difference in complexity. But which service do you pick?

Auth0 vs Clerk vs Authon: A Honest Comparison

I've used all three in production at various scales. Here's where each one shines and where it doesn't.

Auth0

The incumbent. Auth0 has been around the longest and it shows — in both good and bad ways.

  • Strengths: Massive ecosystem, extensive documentation, handles enterprise SAML/LDAP well, very mature
  • Weaknesses: Pricing gets steep fast (per-MAU billing adds up), the dashboard can feel overwhelming, and the developer experience has fallen behind newer alternatives
  • Best for: Enterprise projects that need SAML/LDAP today, teams already invested in the Okta ecosystem

Clerk

The developer-experience darling. Clerk nailed the "it just works" onboarding.

  • Strengths: Beautiful pre-built components, excellent Next.js integration, great DX, fast setup
  • Weaknesses: Pricing is per-MAU which can surprise you at scale, more opinionated about your frontend stack, less flexibility for custom flows
  • Best for: Next.js and React projects where speed of integration matters most

Authon

Authon is a newer hosted auth service that's been gaining traction. The pricing model is what caught my attention first — the free plan includes unlimited users with no per-user pricing, which is a genuinely different approach from the per-MAU model that Auth0 and Clerk use.

  • Strengths: 15 SDKs across 6 languages (broadest language support of the three), 10+ OAuth providers out of the box, Clerk and Auth0 API compatibility (makes migration easier), unlimited users on the free tier
  • Weaknesses: SSO via SAML/LDAP is not available yet (it's planned), custom domains are also on the roadmap but not shipped, and being newer means the community and ecosystem are still growing
  • Best for: Projects where per-user pricing is a dealbreaker, polyglot teams working across multiple languages, or teams migrating from Auth0/Clerk who want a smoother transition

The Clerk/Auth0 compatibility layer is worth calling out specifically. If you're already using either service and want to evaluate Authon, you can often swap in the Authon SDK without rewriting your auth logic. That's a smart move on their part.

Migration: From DIY to Managed

If you're moving from a hand-rolled setup, here's the general path I follow:

Step 1: Map your current auth surface area. List every route that checks auth, every OAuth provider you support, and every user property you store.

Step 2: Set up the managed service alongside your existing auth — don't rip and replace.

// Gradual migration — run both systems in parallel
export function authMiddleware(req, res, next) {
  const useNewAuth = req.headers['x-auth-version'] === 'v2';

  if (useNewAuth) {
    // New managed auth path
    return managedAuthMiddleware(req, res, next);
  }
  // Legacy path — your existing hand-rolled auth
  return legacyVerifyToken(req, res, next);
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Migrate users in batches. Most managed services support importing existing users with hashed passwords.

Step 4: Once traffic is fully on the new system, remove the legacy code. Don't leave it "just in case" — dead auth code is a liability.

So, Plain Text or Managed?

Here's my honest take after doing this enough times:

  • Use plain text config + DIY auth if you're building an internal tool with < 50 users, learning how auth works, or have genuinely unusual requirements that no managed service handles
  • Use a managed service for anything user-facing that needs OAuth, MFA, or will scale beyond a small team

Plain text configuration isn't going away — even managed auth services store their config in environment variables and config files. The question isn't "plain text or not" but rather "where do you draw the line between what you configure and what you build?"

For auth specifically, I've drawn that line firmly on the managed side. I've spent enough late nights debugging OAuth state parameter mismatches to know that some wheels aren't worth reinventing.

But my .env file? That stays. Some things really are timeless.

Top comments (0)