Auth is the part every tutorial skips. Here's a complete, honest breakdown of your three real options in 2026.
Authentication is where most "build an app in a weekend" tutorials quietly stop. They get you to a working UI, a connected database, maybe a deployed URL β and then assume auth is someone else's problem. It isn't. It's yours, and it's the part that most commonly causes production incidents, data breaches, and support tickets.
What you'll learn in this post:
- The three realistic auth implementation paths available in 2026
- Step-by-step walkthrough of a custom JWT system (for developers who want control)
- Honest trade-off comparison of managed auth services (Auth0, Clerk, Supabase Auth)
- When to use an AI app builder that ships auth pre-configured
- The security mistakes developers make on every path
Your Three Options
Before writing a single line of code, it helps to understand what you're choosing between.
Option 1: Roll your own JWT auth. Full control. Maximum flexibility. Highest implementation risk. Appropriate when you have specific session requirements, compliance constraints, or need deep integration with a custom user model.
Option 2: Use a managed auth service. Auth0, Clerk, Supabase Auth, Firebase Auth. Faster to implement, handles the hard parts (token rotation, session management, MFA, OAuth), adds a vendor dependency and a cost curve as you scale.
Option 3: Use a platform that ships auth pre-wired. Full-stack AI builders and opinionated frameworks that include authentication as a default feature, not an add-on. Fastest path for non-technical builders; less flexible for developers with specific requirements.
Each is the right answer in different situations. Here's how to build each one.
Option 1: Custom JWT Authentication (Step by Step)
Custom JWT auth is not as dangerous as it's often portrayed β if implemented correctly. The danger comes from common shortcuts. Here's the correct path.
What You'll Need
- Node.js + Express (or any server framework)
-
jsonwebtokennpm package -
bcryptfor password hashing - A database (Postgres recommended)
-
cookie-parserfor httpOnly cookie storage
Step 1: Hash passwords correctly
Never store plaintext passwords. Never store MD5 or SHA-256 hashed passwords. Use bcrypt with a work factor of 12 or higher.
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
async function hashPassword(plaintext) {
return await bcrypt.hash(plaintext, SALT_ROUNDS);
}
async function verifyPassword(plaintext, hash) {
return await bcrypt.compare(plaintext, hash);
}
A work factor of 12 means each hash takes approximately 250ms on a modern server β slow enough to make brute-force attacks expensive, fast enough to be imperceptible to users at login.
Step 2: Generate and sign JWTs
const jwt = require('jsonwebtoken');
function generateTokens(userId) {
const accessToken = jwt.sign(
{ sub: userId, type: 'access' },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
Keep access tokens short-lived (15 minutes is standard). Use refresh tokens for session continuity. Store refresh tokens in the database so they can be invalidated on logout or compromise.
Step 3: Store tokens securely
The single most common JWT implementation mistake: storing tokens in localStorage.
localStorage is accessible to any JavaScript running on the page. An XSS vulnerability β even a minor one β can exfiltrate every stored token. Use httpOnly cookies instead.
function setAuthCookies(res, { accessToken, refreshToken }) {
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15 minutes
});
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
}
Step 4: Protect routes with middleware
function requireAuth(req, res, next) {
const token = req.cookies.access_token;
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
req.user = { id: decoded.sub };
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
Apply this middleware to any route that requires an authenticated user.
Step 5: Implement token refresh
app.post('/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Verify token exists in database (invalidation check)
const stored = await db.query(
'SELECT * FROM refresh_tokens WHERE token = $1 AND user_id = $2',
[refreshToken, decoded.sub]
);
if (!stored.rows.length) {
return res.status(401).json({ error: 'Refresh token revoked' });
}
const tokens = generateTokens(decoded.sub);
setAuthCookies(res, tokens);
res.json({ success: true });
} catch (err) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Time investment: 4β8 hours to implement correctly, including testing. Plan for additional time for password reset flows, email verification, and OAuth if needed.
Option 2: Managed Auth Services β Honest Comparison
| Feature | Auth0 | Clerk | Supabase Auth | Firebase Auth |
|---|---|---|---|---|
| Free tier | 7,500 MAU | 10,000 MAU | 50,000 MAU | 10,000/month |
| Pricing at 10K MAU | ~$23/month | Free | Free | Free |
| Pricing at 100K MAU | ~$240/month | ~$25/month | ~$25/month | ~$55/month |
| UI components included | Partial | Yes (prebuilt) | Partial | No |
| Social OAuth | Yes | Yes | Yes | Yes |
| MFA support | Yes (add-on) | Yes | Yes | Yes |
| Next.js integration | Good | Excellent | Good | Good |
| Self-hosting option | No | No | Yes | No |
When Auth0 makes sense
Auth0 is the enterprise-grade choice. Its compliance certifications (SOC 2, HIPAA, ISO 27001), extensibility via Actions, and organizational identity federation make it appropriate for applications with enterprise customers or regulatory requirements. The cost curve is steep at scale β at 100,000 MAU, Auth0 is roughly 4β10x more expensive than alternatives.
When Clerk makes sense
Clerk's prebuilt UI components (<SignIn />, <UserButton />, <OrganizationSwitcher />) dramatically reduce implementation time for Next.js applications. A complete Clerk integration β signup, login, session management, user profile β can be implemented in under 30 minutes following their official documentation. The free tier is generous; pricing at scale is competitive.
When Supabase Auth makes sense
For applications already using Supabase as their database, the integrated auth layer removes cross-service complexity. Row-level security policies in Postgres can reference auth.uid() directly, creating tight, verifiable access control at the database level. The self-hosting option is a meaningful advantage for applications with data residency requirements.
Option 3: AI Builders With Auth Pre-Configured
For founders and makers who are not writing the application code themselves, managed auth services still require configuration, environment variable management, webhook setup, and integration testing β tasks that assume developer familiarity.
A third category of tools ships authentication as part of the generated application output, preconfigured and tested. Platforms like imagine.bo generate full-stack applications with auth flows β email/password, OAuth, session management β already wired into the codebase. The database schema includes a users table with correct password hashing defaults; the API routes include protected endpoints from the first generation.
The trade-off is customization ceiling. Pre-configured auth works well for standard patterns β user signup, login, protected routes, basic role management. Applications with unusual session requirements, custom identity federation, or complex permission models may need to modify the generated auth layer or bring in a managed service.
For teams that do hit that ceiling, the hybrid model (AI generation for the standard layer, human engineer for custom requirements) prevents the "rebuild everything" outcome that plagues founders who discover the limitation after launch.
The Security Mistakes That Happen on Every Path
Regardless of implementation approach, these errors appear consistently in production auth systems:
Missing rate limiting on auth endpoints. Login, registration, and password-reset endpoints without rate limiting are open to brute-force and credential-stuffing attacks. Apply rate limiting (
express-rate-limitor equivalent) to all auth routes β 5β10 attempts per 15-minute window is a reasonable default.Verbose error messages. Returning "password incorrect" vs. "user not found" as distinct error messages leaks account existence information. Return a generic "invalid credentials" message for all auth failures.
Password reset tokens that don't expire. Reset tokens should expire within 15β60 minutes and be invalidated after a single use. Storing them as bcrypt hashes in the database prevents database dump attacks.
Missing CSRF protection on session-based auth. httpOnly cookie-based auth requires CSRF token validation on state-changing requests. JWT-based auth in the Authorization header is not susceptible to CSRF β but cookie-based delivery is.
Not invalidating sessions on password change. When a user changes their password (especially via a reset flow), all existing sessions should be invalidated. Failing to do this allows an attacker who briefly had account access to maintain it after the user has recovered the account.
Further Reading
- OWASP Authentication Cheat Sheet β the canonical reference for auth security requirements
- Clerk Documentation β Next.js Quickstart β best-in-class managed auth setup guide
- Supabase Auth Documentation β includes row-level security examples with auth integration
- JWT.io β token debugger and algorithm reference
Frequently Asked Questions
Q: Is JWT better than session-based auth?
A: Neither is universally better. JWT is stateless β the server doesn't need to store session data, which simplifies horizontal scaling. Session-based auth is easier to invalidate immediately (delete the session record). For most SaaS applications, JWT with a short access token expiry and a database-backed refresh token combines the advantages of both.
Q: Can I use Clerk or Auth0 with an AI-generated codebase?
A: Generally yes, with some configuration. Most AI builders generate standard Express or Next.js code that is compatible with managed auth SDKs. The integration complexity depends on how the generated code handles middleware and routing.
Q: Is OAuth (Google, GitHub login) hard to implement?
A: With a managed service like Clerk or Supabase Auth, adding OAuth providers is typically a configuration step (enable provider, add credentials) rather than a code change. Custom JWT implementations require a library like Passport.js and more careful session state management.
Q: What's the minimum auth implementation for an MVP?
A: Email/password login with bcrypt password hashing, httpOnly cookie session storage, basic rate limiting on the login endpoint, and password reset via time-limited email token. This covers the security baseline for a public-facing application without over-engineering for scale that may not arrive.
Q: Should I build auth myself or use a service?
A: Use a managed service unless you have a specific reason not to. The implementation surface for custom auth is large, and the failure modes are serious. The time saved by using Clerk or Supabase Auth is better spent on product features that differentiate the application.













