Auth Providers for Mobile, Web, and API: A Complete Guide to Choosing the Right Solution

Real-world comparison of Auth0, Firebase Auth, Supabase Auth, AWS Cognito, and custom solutions. When to use each, cost analysis, and the debugging nightmares that taught me everything.

Last year, I inherited a mess of authentication systems across our platform. We had Auth0 for the web app, Firebase Auth for mobile, a custom JWT solution for APIs, and three different user databases. When a user tried to access their account from mobile after signing up on web, they got a "user not found" error. That's when I decided to audit every auth provider on the market and consolidate our authentication strategy.

After implementing auth solutions for 15+ production applications and debugging authentication issues at 3 AM more times than I care to admit, here's what I learned about choosing the right auth provider for different scenarios.

The Authentication Landscape: What Actually Works in Production#

Let me start with the brutal truth: there's no perfect auth provider. Each one has trade-offs that become painfully obvious when you're dealing with 100,000+ users and compliance requirements. Here's what I've used in real production environments:

Auth0: The Enterprise Workhorse#

When I use it: Enterprise applications, B2B SaaS, compliance-heavy industries When I avoid it: Startups with tight budgets, simple consumer apps

Real production experience:

TypeScript
// Auth0 configuration that actually works in production
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
    );
  }
};

The good:

  • SOC 2, GDPR, HIPAA compliance out of the box
  • Excellent enterprise features (SAML, LDAP, MFA)
  • Robust admin dashboard for user management
  • Great documentation and support

The ugly:

  • Cost: $23/month for 7,000 users, then $0.00325 per user. At 100k users, that's $300+/month
  • Complexity: Overkill for simple apps
  • Vendor lock-in: Custom rules and hooks tie you to Auth0
  • Performance: Sometimes slow token validation in high-traffic scenarios

Real debugging story: We had a production issue where Auth0 was taking 2+ seconds to validate tokens during peak hours. Turned out we were hitting rate limits because we weren't caching tokens properly. The fix was implementing Redis-based token caching, but that added another dependency to our stack.

Firebase Auth: The Google Ecosystem Choice#

When I use it: Mobile-first apps, Google ecosystem integration, rapid prototyping When I avoid it: Multi-tenant B2B apps, strict compliance requirements

Production configuration:

TypeScript
// 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);
    });
  }
});

The good:

  • Free tier: 10,000 authentications/month free
  • Mobile integration: Excellent React Native support
  • Google services: Seamless integration with Firestore, Functions
  • Simple setup: Can have auth working in 30 minutes

The ugly:

  • Google lock-in: Hard to migrate away from Google ecosystem
  • Limited customization: Can't customize auth flows as much as Auth0
  • Admin limitations: Basic admin dashboard compared to Auth0
  • Compliance: Limited enterprise compliance features

Real cost analysis: For a mobile app with 50k monthly active users, Firebase Auth costs $0 vs Auth0's $50/month. But when you factor in Firestore usage and other Google services, the total cost difference shrinks significantly.

Supabase Auth: The Open Source Alternative#

When I use it: Open source projects, PostgreSQL-heavy stacks, cost-conscious startups When I avoid it: Enterprise compliance requirements, complex multi-tenant scenarios

Production setup:

TypeScript
// 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 };
};

The good:

  • Cost:

$25/month for unlimited users (with usage limits)

  • Open source: Self-hostable if needed
  • PostgreSQL: Direct database access for custom queries
  • Real-time: Built-in real-time subscriptions

The ugly:

  • Maturity: Less mature than Auth0/Firebase
  • Enterprise features: Limited enterprise compliance features
  • Support: Community support vs enterprise support
  • Complexity: Requires more setup for advanced features

Real implementation story: We used Supabase for a startup project and hit the free tier limits quickly. The migration to paid tier was seamless, but we discovered that some advanced features we needed (like custom claims) required more work than expected.

AWS Cognito: The AWS Native Solution#

When I use it: AWS-heavy architectures, serverless applications, cost optimization When I avoid it: Non-AWS environments, rapid prototyping

Production configuration:

TypeScript
// AWS Cognito with CDK
import { UserPool, UserPoolClient, 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),
});

The good:

  • Cost: Very cheap for high-volume applications
  • AWS integration: Seamless with Lambda, API Gateway, etc.
  • Scalability: Handles millions of users
  • Security: AWS-grade security and compliance

The ugly:

  • Complexity: Steep learning curve
  • AWS lock-in: Hard to use outside AWS ecosystem
  • UI: Basic hosted UI, requires custom development
  • Debugging: AWS CloudWatch logs can be overwhelming

Real cost comparison: For 100k users, Cognito costs ~$50/month vs Auth0's $300+/month. But development time and AWS expertise requirements can offset these savings.

Custom JWT Solution: The Full Control Option#

When I use it: Simple applications, learning projects, when you need complete control When I avoid it: Production applications, compliance requirements, team projects

Production implementation:

TypeScript
// Custom JWT auth with proper security
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { randomBytes } from 'crypto';

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');
    }
  }
}

The good:

  • Complete control: Full customization of auth flows
  • Cost: Only infrastructure costs
  • Learning: Great for understanding auth concepts
  • Flexibility: Can implement any auth pattern

The ugly:

  • 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#

FeatureAuth0Firebase AuthSupabase AuthAWS CognitoCustom JWT
Setup Time2-4 hours30 minutes1-2 hours4-8 hours1-2 weeks
Cost (100k users)$300+/month$0-50/month$25/month$50/month$15/month

0/month | | 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 |

Real-World Scenarios: When to Use Each#

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

Real implementation:

TypeScript
// Auth0 enterprise configuration
const auth0Config = {
  domain: process.env.AUTH0_DOMAIN,
  clientId: process.env.AUTH0_CLIENT_ID,
  audience: process.env.AUTH0_AUDIENCE,
  // Enterprise features
  scope: 'openid profile email read:users write:users read:logs',
  // SAML configuration
  samlConfiguration: {
    signInUrl: process.env.SAML_SIGN_IN_URL,
    signOutUrl: process.env.SAML_SIGN_OUT_URL,
  },
  // Custom rules for enterprise logic
  rules: [
    {
      name: 'Add enterprise metadata',
      script: `
        function (user, context, callback) {
          // Add enterprise-specific claims
          context.idToken['https://myapp.com/enterprise'] = user.app_metadata.enterprise;
          callback(null, user, context);
        }
      `
    }
  ]
};

Scenario 2: Mobile-First Consumer App#

Requirements: Social login, push notifications, rapid development Choice: Firebase Auth Why: Excellent mobile integration, free tier, Google ecosystem

Real implementation:

TypeScript
// 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: Unlimited users for

$25/month, direct database access

Real implementation:

TypeScript
// Supabase Auth with custom user metadata
const { data: { user }, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  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, very low cost at scale

Real implementation:

TypeScript
// 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 {
    const token = event.headers.Authorization?.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

Cost Analysis: Real Numbers from Production#

Let me break down the actual costs I've seen in production:

Auth0 Cost Breakdown#

  • Free tier: 7,000 users
  • Growth plan: $23/month + $0.00325 per user
  • Enterprise plan: Custom pricing
  • Real example: 50k users = $23 + (43k × $0.00325) = $163/month

62.75/month

Firebase Auth Cost Breakdown#

  • Free tier: 10,000 authentications/month
  • Paid tier: $0.01 per authentication after free tier
  • Real example: 50k authentications/month = $0 (within free tier)

Supabase Auth Cost Breakdown#

  • Free tier: 50,000 users
  • Pro plan:

$25/month for unlimited users

  • Real example: 100k users = $25/month

AWS Cognito Cost Breakdown#

  • User pools: $0.0055 per MAU
  • Identity pools: $0.0055 per MAU
  • Real example: 50k MAU = $175/month

Migration Strategies: Lessons from Real Migrations#

I've migrated between auth providers multiple times. Here are the strategies that actually work:

Migration from Custom JWT to Auth0#

TypeScript
// Migration script for user data
const migrateUsersToAuth0 = async () => {
  const users = await getUsersFromCustomDB();

  for (const user of users) {
    try {
      // Create user in Auth0
      const auth0User = await auth0Management.users.create({
        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#

TypeScript
// Firebase to Auth0 migration
const migrateFromFirebase = async () => {
  const firebaseUsers = await getFirebaseUsers();

  for (const firebaseUser of firebaseUsers) {
    try {
      // Create user in Auth0
      const auth0User = await auth0Management.users.create({
        email: firebaseUser.email,
        email_verified: firebaseUser.emailVerified,
        user_metadata: {
          firebase_uid: firebaseUser.uid,
          migrated_at: new Date().toISOString()
        }
      });

      // Migrate custom claims
      if (firebaseUser.customClaims) {
        await auth0Management.users.update(
          { user_id: auth0User.user_id },
          { app_metadata: firebaseUser.customClaims }
        );
      }

    } catch (error) {
      console.error(`Migration failed for ${firebaseUser.email}:`, error);
    }
  }
};

Security Considerations: What I've Learned the Hard Way#

Token Security#

TypeScript
// Secure token handling
const secureTokenStorage = {
  // Store tokens securely
  storeTokens: (accessToken: string, refreshToken: string) => {
    // Use secure storage (not localStorage for sensitive data)
    if (isMobile()) {
      // Use Keychain (iOS) or Keystore (Android)
      SecureStore.setItemAsync('access_token', accessToken);
      SecureStore.setItemAsync('refresh_token', refreshToken);
    } else {
      // Use httpOnly cookies for web
      document.cookie = `access_token=${accessToken}; HttpOnly; Secure; SameSite=Strict`;
    }
  },

  // Rotate tokens regularly
  rotateTokens: async () => {
    const refreshToken = await getRefreshToken();
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${refreshToken}` }
    });

    if (response.ok) {
      const { accessToken, refreshToken: newRefreshToken } = await response.json();
      secureTokenStorage.storeTokens(accessToken, newRefreshToken);
    }
  }
};

Rate Limiting#

TypeScript
// Rate limiting for auth endpoints
const rateLimit = require('express-rate-limit');

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 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({
    client: redisClient,
    prefix: 'auth_rate_limit:'
  })
});

app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);

Performance Optimization: Lessons from High-Traffic Apps#

Token Caching#

TypeScript
// Redis-based token caching
class TokenCache {
  private redis: Redis;
  private readonly CACHE_TTL = 3600; // 1 hour

  constructor() {
    this.redis = new Redis(process.env.REDIS_URL);
  }

  async cacheToken(userId: string, token: string): Promise<void> {
    await this.redis.setex(`token:${userId}`, this.CACHE_TTL, token);
  }

  async getCachedToken(userId: string): Promise<string | null> {
    return await this.redis.get(`token:${userId}`);
  }

  async invalidateToken(userId: string): Promise<void> {
    await this.redis.del(`token:${userId}`);
  }
}

Connection Pooling#

TypeScript
// Database connection pooling for auth
const pool = new Pool({
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT),
  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,
  // Enable SSL for production
  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});

Debugging Authentication Issues: Real Stories#

The Case of the Disappearing Users#

Problem: Users were being created in Auth0 but not appearing in our database Root cause: Race condition between Auth0 webhook and user creation Solution: Implemented idempotent user creation with proper error handling

TypeScript
// 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;
  }
};

The Token Validation Mystery#

Problem: API calls were failing with "invalid token" errors intermittently Root cause: Clock skew between servers and Auth0 Solution: Implemented token validation with clock skew tolerance

TypeScript
// 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');
  }
};

Final Recommendations: What I'd Do Differently#

For New Projects#

  1. Start with Firebase Auth if you're building a mobile-first consumer app
  2. Use Supabase Auth if you're cost-conscious and PostgreSQL-heavy
  3. Go with Auth0 if you need enterprise features from day one
  4. Choose AWS Cognito if you're already heavily invested in AWS

For Existing Projects#

  1. Don't migrate unless necessary - auth migrations are painful
  2. Implement proper monitoring before making changes
  3. Plan for gradual migration with dual auth systems
  4. Test thoroughly - auth bugs are the worst kind of bugs

Cost Optimization Strategies#

  1. Monitor usage patterns and optimize accordingly
  2. Implement proper caching to reduce auth provider calls
  3. Use refresh tokens to minimize token generation
  4. Consider hybrid approaches for different user segments

Security Best Practices#

  1. Always verify tokens on the server side - never trust client-side validation
  2. Implement proper session management with secure token storage
  3. Use HTTPS everywhere - especially for auth endpoints
  4. Regular security audits of your auth implementation
  5. Monitor for suspicious activity and implement rate limiting

Performance Optimization Tips#

  1. Cache user sessions to reduce database queries
  2. Use connection pooling for database connections
  3. Implement token caching to reduce auth provider API calls
  4. Optimize token validation with proper algorithms and caching
  5. Monitor auth performance and optimize bottlenecks

Team Considerations#

  1. Choose based on team expertise - don't pick Cognito if no one knows AWS
  2. Consider maintenance burden - custom solutions require ongoing work
  3. Plan for team growth - auth systems should scale with your team
  4. Document everything - auth is critical infrastructure
  5. Have a backup plan - always know how to migrate if needed

Conclusion#

Choosing the right auth provider is more about understanding your specific requirements than finding the "best" solution. Each provider has its strengths and weaknesses, and the right choice depends on your team's expertise, budget, compliance requirements, and technical constraints.

The most important lesson I've learned is to start simple and evolve your auth strategy as your application grows. Don't over-engineer authentication from day one, but also don't ignore the security and scalability implications of your choice.

Remember: authentication is not just about security—it's about user experience, developer experience, and business requirements. Choose wisely, implement carefully, and always have a backup plan.

Loading...

Comments (0)

Join the conversation

Sign in to share your thoughts and engage with the community

No comments yet

Be the first to share your thoughts on this post!

Related Posts