Auth0 vs Firebase Auth vs Cognito vs Supabase Auth: Which to Choose
Compare Auth0, Firebase Auth, Supabase Auth, AWS Cognito, and custom JWT: which to default to, how the pricing models differ, and the pitfalls to plan for.
Authentication provider choice sets development velocity, compliance ceiling, and monthly bill for years. Swapping providers later is one of the riskiest migrations a product can run. The default worth starting from is the managed provider already native to your primary platform: Firebase Auth for mobile-first consumer apps, Supabase Auth for PostgreSQL-backed products, Cognito inside AWS-native serverless stacks, and Auth0 when SAML/SSO and compliance artifacts are contract requirements. Custom JWT stays a learning exercise.
A common starting point looks nothing like that: Auth0 for the web app, Firebase Auth for mobile, custom JWT for the API, and three separate user tables. Users register on the web, then hit “user not found” on mobile, and consolidation stops being optional. Six dimensions decide which provider you consolidate on, and cost is only the first of them.
The Six Decision Dimensions
Each dimension can veto a provider on its own, which is why price alone makes a poor tiebreaker:
1. Cost Structure Analysis
- Fixed Costs: Base subscription fees and setup expenses
- Variable Costs: Per-user, per-authentication, or usage-based pricing
- Hidden Costs: Development time, maintenance overhead, migration expenses
- Scale Economics: Cost behavior at 10K, 50K, and 100K+ users
2. Technical Integration Assessment
- Setup Complexity: Time to production-ready implementation
- Platform Support: Web, mobile (iOS/Android), API compatibility
- Customization Depth: Authentication flow modification capabilities
- Vendor Lock-in Risk: Migration difficulty and data portability
3. Enterprise Readiness
- Compliance Coverage: SOC 2, GDPR, HIPAA, industry-specific requirements
- Enterprise Features: SAML/SSO, multi-tenancy, audit logging
- Security Posture: MFA options, threat detection, security certifications
- Support Quality: Documentation, community, enterprise support tiers
4. Operational Characteristics
- Reliability Metrics: SLA commitments, historical uptime
- Performance Impact: Latency, throughput, caching capabilities
- Monitoring Integration: Observability, debugging tools
- Maintenance Burden: Updates, security patches, operational overhead
5. Developer Experience
- API Quality: SDK completeness, documentation clarity
- Learning Curve: Onboarding time for development teams
- Debugging Tools: Error handling, logging, development environments
- Community Ecosystem: Third-party integrations, community support
6. Strategic Alignment
- Technology Stack Compatibility: Ecosystem integration benefits
- Organizational Capability: Required expertise and team skills
- Growth Trajectory: Scaling characteristics and future requirements
- Risk Tolerance: Vendor dependency, technical debt implications
Provider Analysis
Each provider wins a different subset of those dimensions:
Auth0: Enterprise-Grade Authentication Platform
Optimal Use Cases: Enterprise B2B applications, compliance-regulated industries, organizations requiring extensive SSO integration Avoid When: Cost-sensitive early-stage applications, simple authentication requirements
Client configuration:
// Auth0 SPA client configuration
const auth0Config = {
domain: process.env.AUTH0_DOMAIN,
clientId: process.env.AUTH0_CLIENT_ID,
audience: process.env.AUTH0_AUDIENCE,
// Critical: Set proper scopes for API access
scope: 'openid profile email read:users write:users',
// Cache tokens properly to avoid rate limits
cacheLocation: 'localstorage',
useRefreshTokens: true,
// Handle token expiration gracefully
onRedirectCallback: (appState) => {
window.history.replaceState(
{},
document.title,
appState?.returnTo || window.location.pathname
);
}
};
Strengths:
- Compliance Foundation: SOC 2, GDPR, HIPAA compliance out of the box
- Enterprise Features: Comprehensive SAML, LDAP, MFA, and SSO capabilities
- Management Interface: Robust admin dashboard with advanced user management
- Support Ecosystem: Extensive documentation and enterprise-grade support
Limitations:
- Cost Structure: Free up to 25,000 MAU, then B2C Essentials from $35/month at 500 MAU or Professional from $240/month. Self-service B2C plans stop at 50,000 MAU and B2B plans at 20,000 MAU; past those lines you are negotiating an Enterprise contract
- Complexity Overhead: Feature richness creates unnecessary complexity for simple use cases
- Vendor Lock-in: Extensive customization through Actions increases migration difficulty, and the older Rules and Hooks were deprecated in favor of Actions, so legacy integrations need rewriting before they can be ported
- Performance Variability: Token validation latency can increase under high concurrent load
Where teams get caught: Latency on token validation climbs once a client requests a fresh token on every call and starts colliding with Auth0’s tenant rate limits. Cache the token in the SDK and reuse it until expiry; for machine-to-machine credentials, a shared Redis cache does the same job server-side at the cost of one more dependency.
Firebase Auth: Google-Integrated Mobile-First Solution
Optimal Use Cases: Mobile-first consumer applications, Google Cloud ecosystem integration, rapid prototyping Avoid When: Multi-tenant B2B requirements, strict enterprise compliance needs, non-Google cloud environments
Production configuration:
// Firebase Auth setup for React Native + Web
import { initializeApp } from 'firebase/app';
import { getAuth, connectAuthEmulator } from 'firebase/auth';
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
// Critical: Don't expose these in client-side code
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
// Production-ready error handling
auth.onAuthStateChanged((user) => {
if (user) {
// Always verify token on server side
user.getIdToken(true).then((token) => {
// Send to your backend for verification
verifyTokenOnServer(token);
});
}
});
Strengths:
- Cost Efficiency: The Spark plan covers 50,000 monthly active users at no charge
- Mobile Excellence: Native iOS/Android SDKs with React Native support
- Ecosystem Integration: Seamless connection to Google Cloud services
- Rapid Deployment: Email and social sign-in wired up in an afternoon, not a sprint
Limitations:
- Ecosystem Lock-in: Migration away from Google services creates complexity
- Customization Constraints: Less flexible authentication flow customization than Auth0
- Administrative Features: Basic management interface compared to enterprise solutions
- Compliance Gaps: Limited enterprise compliance and audit capabilities, and SAML/OIDC federation drops the free allowance from 50,000 MAU to 50
Cost Analysis: At 50,000 monthly active users Firebase Auth still costs nothing, while Auth0 has left its 25,000 MAU free plan and Cognito Essentials is billing 40,000 MAU. Past 50,000 the meter switches to Google Cloud Identity Platform rates on the Blaze plan. Auth is rarely the expensive line either way: the Firestore reads behind a session usually cost more than the sign-in itself.
Supabase Auth: Open-Source PostgreSQL-Native Platform
Optimal Use Cases: PostgreSQL-centric architectures, cost-conscious startups, open-source projects requiring self-hosting options Avoid When: Enterprise compliance mandates, complex multi-tenant architectures, mission-critical production workloads
Production setup:
// Supabase Auth with proper error handling
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
// Production-ready auth hooks
export const useAuth = () => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
setLoading(false);
});
// Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
setUser(session?.user ?? null);
setLoading(false);
}
);
return () => subscription.unsubscribe();
}, []);
return { user, loading };
};
Strengths:
- Cost Structure: $25/month for up to 100,000 monthly active users
- Open Source: Self-hosting capability with full source code access
- Database Integration: Direct PostgreSQL access for custom authentication logic
- Real-time Features: Built-in WebSocket subscriptions for live updates
Limitations:
- Platform Maturity: Less mature ecosystem compared to Auth0/Firebase
- Enterprise Features: Limited enterprise compliance and audit capabilities
- Support Model: Community-driven support versus dedicated enterprise support
- Configuration Complexity: Advanced features require more manual setup
Scaling notes: The free plan carries 50,000 MAU, which covers most products until they have revenue. Pro at $25/month includes 100,000 MAU, so the auth bill stays flat through the range where Auth0 hands you to a sales team. Advanced work such as custom JWT claims is code you write yourself.
AWS Cognito: Cloud-Native Identity Management
Optimal Use Cases: AWS-centric architectures, serverless applications, high-scale cost optimization Avoid When: Multi-cloud deployments, rapid prototyping requirements, teams lacking AWS expertise
Infrastructure definition:
// AWS Cognito with CDK
import { UserPool, Mfa, AccountRecovery } from 'aws-cdk-lib/aws-cognito';
import { Duration } from 'aws-cdk-lib';
const userPool = new UserPool(this, 'MyUserPool', {
userPoolName: 'my-app-users',
selfSignUpEnabled: true,
signInAliases: {
email: true,
phone: true,
},
standardAttributes: {
email: {
required: true,
mutable: true,
},
},
passwordPolicy: {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
},
accountRecovery: AccountRecovery.EMAIL_ONLY,
// Critical for production: Enable MFA
mfa: Mfa.REQUIRED,
mfaSecondFactor: {
sms: true,
otp: true,
},
// Token configuration
accessTokenValidity: Duration.hours(1),
idTokenValidity: Duration.hours(1),
refreshTokenValidity: Duration.days(30),
});
Strengths:
- Cost Efficiency: Three tiers since November 2024. Lite and Essentials include 10,000 MAU at no charge, and user pools created after 22 November 2024 get 50,000 on Lite
- AWS Integration: Native integration with Lambda, API Gateway, and AWS services
- Infinite Scale: Automatic scaling to millions of users
- Security Foundation: AWS security infrastructure and compliance certifications
Limitations:
- Learning Curve: Steep learning curve requiring AWS expertise
- Ecosystem Lock-in: Difficult to implement outside AWS infrastructure
- Interface Limitations: Basic hosted UI requiring custom frontend development
- Operational Complexity: CloudWatch logging and debugging can overwhelm teams
Cost Analysis: The tier you pick moves the bill by nearly 3x. At 100,000 MAU, Lite bills 90,000 users at $0.0055 (about $495/month) while Essentials bills the same users at $0.015 (about $1,350/month), and Plus carries no free allowance at all. SMS and email delivery are billed separately through SNS and SES, so SMS-based MFA becomes its own line item.
Custom JWT Solution: The Full Control Option
Optimal Use Cases: Simple applications, learning projects, situations requiring complete control Avoid When: Production applications, compliance requirements, team projects
Minimal implementation:
// Custom JWT auth with proper security
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
class CustomAuthService {
private readonly JWT_SECRET = process.env.JWT_SECRET!;
private readonly JWT_EXPIRES_IN = '1h';
private readonly REFRESH_TOKEN_EXPIRES_IN = '7d';
async generateTokens(userId: string, email: string) {
const accessToken = jwt.sign(
{ userId, email, type: 'access' },
this.JWT_SECRET,
{ expiresIn: this.JWT_EXPIRES_IN }
);
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
this.JWT_SECRET,
{ expiresIn: this.REFRESH_TOKEN_EXPIRES_IN }
);
// Store refresh token hash in database
const refreshTokenHash = await bcrypt.hash(refreshToken, 12);
await this.storeRefreshToken(userId, refreshTokenHash);
return { accessToken, refreshToken };
}
async verifyToken(token: string) {
try {
const decoded = jwt.verify(token, this.JWT_SECRET) as any;
// Check if token is blacklisted
const isBlacklisted = await this.isTokenBlacklisted(token);
if (isBlacklisted) {
throw new Error('Token is blacklisted');
}
return decoded;
} catch (error) {
throw new Error('Invalid token');
}
}
async refreshAccessToken(refreshToken: string) {
try {
const decoded = jwt.verify(refreshToken, this.JWT_SECRET) as any;
// Verify refresh token exists in database
const isValid = await this.verifyRefreshToken(decoded.userId, refreshToken);
if (!isValid) {
throw new Error('Invalid refresh token');
}
// Generate new access token
const user = await this.getUserById(decoded.userId);
return this.generateTokens(user.id, user.email);
} catch (error) {
throw new Error('Invalid refresh token');
}
}
}
Strengths:
- Complete control: Full customization of auth flows
- Cost: Only infrastructure costs
- Learning: Great for understanding auth concepts
- Flexibility: Can implement any auth pattern
Limitations:
- Security risks: Easy to make security mistakes
- Maintenance: You’re responsible for everything
- Compliance: No built-in compliance features
- Time investment: Significant development time required
Detailed Comparison Matrix
| Feature | Auth0 | Firebase Auth | Supabase Auth | AWS Cognito | Custom JWT |
|---|---|---|---|---|---|
| Setup Time | 2-4 hours | 30 minutes | 1-2 hours | 4-8 hours | 1-2 weeks |
| Cost (100k MAU) | Enterprise quote | Identity Platform rates | $25/month | $495-$1,350/month | Infrastructure only |
| Mobile Support | Excellent | Excellent | Good | Good | Manual |
| Web Support | Excellent | Good | Excellent | Basic | Manual |
| API Support | Excellent | Good | Good | Excellent | Manual |
| Enterprise Features | Excellent | Basic | Limited | Good | Manual |
| Compliance | SOC2, GDPR, HIPAA | Basic | Limited | SOC2, GDPR | Manual |
| Customization | High | Medium | High | Medium | Unlimited |
| Vendor Lock-in | High | High | Medium | High | None |
| Learning Curve | Medium | Low | Medium | High | High |
Matching Scenarios to Providers
Scenario 1: B2B SaaS with Enterprise Customers
Requirements: SAML/SSO, compliance, user management, audit logs Choice: Auth0 Why: Enterprise features, compliance out of the box, excellent admin dashboard
SAML connections are configured in the Auth0 tenant. What belongs in application code is the per-tenant claim the app reads after login, and that goes in a post-login Action:
// Auth0 post-login Action: attach the enterprise tenant to the ID token
exports.onExecutePostLogin = async (event, api) => {
const tenant = event.user.app_metadata?.enterprise;
if (!tenant) {
return;
}
api.idToken.setCustomClaim('https://myapp.com/enterprise', tenant);
api.accessToken.setCustomClaim('https://myapp.com/enterprise', tenant);
};
Scenario 2: Mobile-First Consumer App
Requirements: Social login, push notifications, rapid development Choice: Firebase Auth Why: Excellent mobile integration, free tier, Google ecosystem
Social login setup:
// Firebase Auth with social login
import {
signInWithPopup,
GoogleAuthProvider,
FacebookAuthProvider
} from 'firebase/auth';
const googleProvider = new GoogleAuthProvider();
const facebookProvider = new FacebookAuthProvider();
// Configure providers
googleProvider.addScope('email');
googleProvider.addScope('profile');
facebookProvider.addScope('email');
// Social login implementation
const signInWithGoogle = async () => {
try {
const result = await signInWithPopup(auth, googleProvider);
const user = result.user;
// Send token to backend for verification
const token = await user.getIdToken();
await verifyTokenOnBackend(token);
return user;
} catch (error) {
console.error('Google sign-in error:', error);
throw error;
}
};
Scenario 3: Cost-Conscious Startup
Requirements: Low cost, PostgreSQL integration, rapid iteration Choice: Supabase Auth Why: 100,000 MAU inside the $25/month Pro plan, plus direct SQL access to the users table
Signup with custom metadata:
// Supabase Auth with custom user metadata
const { data: { user }, error } = await supabase.auth.signUp({
email: '[email protected]',
password: 'securepassword',
options: {
data: {
full_name: 'John Doe',
company: 'Startup Inc',
role: 'admin'
}
}
});
// Direct database queries for custom logic
const { data: users, error } = await supabase
.from('users')
.select('*')
.eq('company_id', companyId)
.order('created_at', { ascending: false });
Scenario 4: AWS-Heavy Architecture
Requirements: Serverless, cost optimization, AWS integration Choice: AWS Cognito Why: Seamless Lambda integration, Lite tier pricing that stays predictable at scale
Token verification in Lambda:
// Cognito with Lambda triggers
import { CognitoJwtVerifier } from 'aws-jwt-verify';
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USER_POOL_ID!,
tokenUse: 'access',
clientId: process.env.COGNITO_CLIENT_ID!,
});
// Lambda function with Cognito auth
export const handler = async (event) => {
try {
// API Gateway v2 lowercases header names; v1 preserves the original case
const header = event.headers.authorization ?? event.headers.Authorization;
const token = header?.replace('Bearer ', '');
const payload = await verifier.verify(token);
// User is authenticated, proceed with business logic
const userId = payload.sub;
const result = await processUserRequest(userId, event.body);
return {
statusCode: 200,
body: JSON.stringify(result)
};
} catch (error) {
return {
statusCode: 401,
body: JSON.stringify({ error: 'Unauthorized' })
};
}
};
Scenario 5: Learning Project or Simple App
Requirements: Understanding auth concepts, complete control Choice: Custom JWT Solution Why: Educational value, no vendor dependencies
Pricing Models Side by Side
Every provider bills a different unit, which is why headline comparisons mislead. These are 2025 list prices from the four pricing pages linked in the references.
Auth0
- Free plan: 25,000 monthly active users
- B2C plans: Essentials from $35/month at 500 MAU; Professional from $240/month
- B2B plans: Essentials from $150/month; Professional from $800/month
- Self-service ceiling: 50,000 MAU for B2C and 20,000 MAU for B2B, after which pricing is quoted per Enterprise contract
Firebase Auth
- Free tier: 50,000 monthly active users on the Spark plan
- Beyond the free tier: billed at Google Cloud Identity Platform rates on Blaze
- SAML/OIDC federation: 50 MAU free, then Identity Platform rates for every federated user
Supabase Auth
- Free plan: 50,000 monthly active users
- Pro plan: $25/month including 100,000 MAU
- Beyond the included MAU: $0.00325 per MAU
- Worked example: 150,000 MAU = $25 + (50,000 × $0.00325) = $187.50/month
AWS Cognito
- Lite: 10,000 MAU free, or 50,000 for user pools created after 22 November 2024, then $0.0055 per MAU up to 100,000
- Essentials: 10,000 MAU free, then $0.015 per MAU
- Plus: no free allowance, $0.020 per MAU
- Federated users: SAML and OIDC identities cost $0.015 per MAU above 50 free on every tier
- Not included: SMS and email delivery, billed through SNS and SES
Migration Strategies
Provider migrations carry real risk: every user has to come out the other side with working credentials, and a half-finished migration locks people out of their own accounts. Two shapes cover most cases.
Migration from Custom JWT to Auth0
// Migration script for user data
const migrateUsersToAuth0 = async () => {
const users = await getUsersFromCustomDB();
for (const user of users) {
try {
// Create user in Auth0. `connection` is required: it names the
// database connection the user is created in.
const { data: auth0User } = await auth0Management.users.create({
connection: 'Username-Password-Authentication',
email: user.email,
password: generateTemporaryPassword(),
email_verified: user.emailVerified,
user_metadata: {
migrated_from: 'custom_jwt',
original_user_id: user.id
}
});
// Update local database with Auth0 user ID
await updateUserAuth0Id(user.id, auth0User.user_id);
console.log(`Migrated user: ${user.email}`);
} catch (error) {
console.error(`Failed to migrate user ${user.email}:`, error);
}
}
};
Migration from Firebase to Auth0
Passwords are the hard part. The loop below moves profiles and claims, but it does not move credentials; for that, export the Firebase password hashes with firebase auth:export and load them through Auth0’s bulk user import, which accepts scrypt hashes. Creating users without credentials forces a password reset on your entire base, which generates a support queue nobody planned for.
// Firebase to Auth0 migration
const migrateFromFirebase = async () => {
const firebaseUsers = await getFirebaseUsers();
for (const firebaseUser of firebaseUsers) {
try {
// Create user in Auth0
const { data: auth0User } = await auth0Management.users.create({
connection: 'Username-Password-Authentication',
email: firebaseUser.email,
email_verified: firebaseUser.emailVerified,
user_metadata: {
firebase_uid: firebaseUser.uid,
migrated_at: new Date().toISOString()
}
});
// Migrate custom claims. The identifier parameter is `id`, not `user_id`.
if (firebaseUser.customClaims) {
await auth0Management.users.update(
{ id: auth0User.user_id },
{ app_metadata: firebaseUser.customClaims }
);
}
} catch (error) {
console.error(`Migration failed for ${firebaseUser.email}:`, error);
}
}
};
Security Considerations: Production Patterns
Token Storage
Web and native clients need different answers. Native apps have a platform keystore. Browsers do not, and the common workaround is wrong: document.cookie cannot set an HttpOnly cookie, because the whole point of the flag is that JavaScript is locked out. Only the server can issue one, via Set-Cookie.
// Secure token handling
const secureTokenStorage = {
// Native clients: Keychain on iOS, Keystore on Android
storeMobileTokens: async (accessToken: string, refreshToken: string) => {
await SecureStore.setItemAsync('access_token', accessToken);
await SecureStore.setItemAsync('refresh_token', refreshToken);
},
// Web: the browser never holds the refresh token. The backend exchanges the
// authorization code and returns Set-Cookie with HttpOnly, Secure, SameSite.
exchangeCodeForSession: async (code: string, codeVerifier: string) => {
const response = await fetch('/api/auth/callback', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, codeVerifier })
});
return response.ok;
},
// Rotation runs against the cookie, so no token passes through JavaScript
rotateSession: async () => {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
return response.ok;
}
};
Rate Limiting
// Rate limiting for auth endpoints
import rateLimit from 'express-rate-limit';
import { RedisStore } from 'rate-limit-redis';
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 5, // 5 attempts per window
message: 'Too many authentication attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
// Store rate limit data in Redis for distributed systems
store: new RedisStore({
sendCommand: (...args: string[]) => redisClient.sendCommand(args),
prefix: 'auth_rate_limit:'
})
});
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
Performance Optimization
Machine-to-Machine Token Caching
This is the cache that fixes the Auth0 rate-limit problem described earlier. It holds client-credentials tokens for backend services only; end-user tokens have no business in a shared cache. Every entry is keyed by issuer, client ID, audience, and the normalized scope set, because several services share this Redis. Keying by audience alone would hand one service a token minted with another client’s privileges. The entry expires slightly before the token does, so no request travels with a token that dies in flight.
// Redis-backed cache for client-credentials tokens
import Redis from 'ioredis';
import { createHash } from 'node:crypto';
interface TokenRequest {
issuer: string;
clientId: string;
audience: string;
scopes: string[];
}
class M2MTokenCache {
private redis: Redis;
private readonly EXPIRY_SKEW_SECONDS = 60;
constructor() {
this.redis = new Redis(process.env.REDIS_URL!);
}
// The key covers issuer, client, audience, and the normalized scope set.
// Hashing avoids a delimiter clash with scope names like `read:users`.
private cacheKey(request: TokenRequest): string {
const scopes = [...new Set(request.scopes)].sort().join(' ');
const fingerprint = createHash('sha256')
.update([request.issuer, request.clientId, request.audience, scopes].join('\n'))
.digest('hex');
return `m2m:${fingerprint}`;
}
async cacheToken(request: TokenRequest, token: string, expiresIn: number): Promise<void> {
const ttl = Math.max(expiresIn - this.EXPIRY_SKEW_SECONDS, 1);
await this.redis.setex(this.cacheKey(request), ttl, token);
}
async getCachedToken(request: TokenRequest): Promise<string | null> {
return await this.redis.get(this.cacheKey(request));
}
async invalidate(request: TokenRequest): Promise<void> {
await this.redis.del(this.cacheKey(request));
}
}
Connection Pooling
// Database connection pooling for auth
const pool = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT ?? '5432', 10),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
// Optimize for auth queries
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
// Verify the server certificate in production. Setting rejectUnauthorized
// to false here would undo the point of enabling TLS.
ssl: process.env.NODE_ENV === 'production'
? { rejectUnauthorized: true, ca: process.env.DB_CA_CERT }
: false
});
Common Pitfalls
Webhook Race: Users Exist at the Provider but Not in Your Database
Symptom: Accounts appear in the Auth0 dashboard, and the application database has no matching row Root cause: The post-registration webhook and the first authenticated request both try to create the local user Fix: Make local user creation idempotent and treat the unique-constraint violation as a successful outcome
// Idempotent user creation
const createUserIfNotExists = async (auth0User: any) => {
const existingUser = await db.user.findUnique({
where: { auth0Id: auth0User.user_id }
});
if (existingUser) {
return existingUser;
}
try {
return await db.user.create({
data: {
auth0Id: auth0User.user_id,
email: auth0User.email,
emailVerified: auth0User.email_verified,
metadata: auth0User.user_metadata
}
});
} catch (error) {
// Handle race condition
if (error.code === 'P2002') {
return await db.user.findUnique({
where: { auth0Id: auth0User.user_id }
});
}
throw error;
}
};
Clock Skew: Intermittent Invalid-Token Errors
Symptom: A small fraction of API calls fail token validation, and retrying the same request succeeds
Root cause: The verifying server’s clock drifts from the issuer’s, so nbf or exp is evaluated against the wrong second
Fix: Allow a bounded clock tolerance during verification and keep NTP running on every host
// Token validation with clock skew tolerance
const validateToken = async (token: string) => {
try {
const decoded = jwt.verify(token, process.env.AUTH0_PUBLIC_KEY, {
algorithms: ['RS256'],
clockTolerance: 30, // 30 seconds tolerance
issuer: `https://${process.env.AUTH0_DOMAIN}/`,
audience: process.env.AUTH0_AUDIENCE
});
return decoded;
} catch (error) {
console.error('Token validation error:', error);
throw new Error('Invalid token');
}
};
Decision Framework
Signals That Pick the Provider
One signal usually dominates. Find yours before comparing feature grids:
- The product is a mobile consumer app and your analytics already sit in Google Cloud → Firebase Auth
- Product data lives in PostgreSQL and the team is comfortable writing SQL → Supabase Auth
- Compute is Lambda behind API Gateway and IAM already gates everything else → Cognito, on the Lite tier until a feature forces Essentials
- A signed contract names SAML, SCIM, or an audit report → Auth0, and budget for the Enterprise conversation above 50,000 MAU
- Two signals fire at once → follow the one attached to your primary platform, because that is the integration you will maintain daily
For Existing Projects
- Avoid migration unless something forces it: Authentication migrations carry risk out of proportion to their visible scope
- Baseline before you move: Measure current login success rate and latency, or you will have no way to tell whether the new provider is worse
- Run both providers during the cutover: Verify against the new provider first and fall back to the old one, then remove the fallback once the fallback rate reaches zero
- Export credentials alongside profiles: A migration that forces a global password reset has failed, whatever the user table says
Implementation Guardrails
- Verify tokens on the server: Client-side validation tells you what the client wants you to believe
- Keep refresh tokens out of JavaScript: httpOnly cookies on web, platform keystore on native
- Rate limit login and registration separately: Credential stuffing and enumeration have different shapes
- Cache machine-to-machine tokens only: This is where provider rate limits actually bite, and user tokens have no place in a shared cache
- Budget from measured MAU: Every provider counts monthly active users slightly differently, and federated identities are frequently priced apart from local ones
- Write down the exit path: Knowing how you would leave is cheaper than discovering it under pressure
Provider pricing changes often. The figures above are 2025 list prices from the pricing pages in the references, and total cost of ownership includes development time and eventual migration on top of the per-MAU line.
The platform-native default holds for as long as one platform dominates the product. Three things should move it: a contract that names an identity requirement your provider cannot meet, a second platform growing large enough that neither ecosystem is primary, or an MAU curve crossing a self-service ceiling where the Enterprise quote exceeds the cost of migrating. None of the three is a reason to build your own.
References
- RFC 6749: The OAuth 2.0 Authorization Framework - The IETF specification defining the OAuth 2.0 authorization framework that all modern auth providers implement.
- OpenID Connect Core 1.0 Specification - The official OpenID Foundation specification for OIDC, the identity layer built on top of OAuth 2.0.
- RFC 7519: JSON Web Token (JWT) - The IETF RFC defining the JWT structure, claims, and signing mechanisms used by all major auth providers.
- Amazon Cognito User Pools Documentation - Official AWS Cognito developer guide covering user pools, identity federation, and OIDC integration.
- Amazon Cognito: Authentication with User Pools - Official Cognito guide on authentication flows, including the user pools API and OAuth 2.0 authorization server modes.
- RFC 8725: JSON Web Token Best Current Practices - IETF document providing security best practices for implementing and validating JWTs.
- How OpenID Connect Works - The OpenID Foundation’s overview of OIDC authentication flows, tokens, and discovery mechanisms.
- Auth0 Pricing - Free plan limits, B2C and B2B plan tiers, and the MAU ceilings where self-service ends and Enterprise pricing begins.
- Firebase Pricing - Spark and Blaze plan limits for Firebase Authentication, including the separate allowance for SAML and OIDC sign-in.
- Supabase Pricing - Free and Pro plan MAU allowances plus the per-MAU rate applied beyond the included quota.
- Amazon Cognito Pricing - The Lite, Essentials, and Plus tiers, their free MAU allowances, per-MAU rates, and federated identity pricing.
Related posts
A technical guide to advanced Amazon Cognito: custom auth flows, federation, multi-tenancy, migration strategies, and production-grade security with CDK.
HMAC-SHA-256 for webhooks, signed URLs, and internal auth, with runnable code in three languages and the boundary where digital signatures take over.
Build SaaS authorization with AWS Cognito and Verified Permissions, covering Cedar policies, multi-tenant patterns, JWT flow, and cost in TypeScript.
Build, secure, and deploy custom Model Context Protocol servers for internal systems in TypeScript, with authentication, monitoring, and Kubernetes deployment.
Definitions, implementation context, and defaults for authentication, token, access control, and Zero Trust terminology that teams argue about.