You need authentication. Every web application needs it. And choosing the wrong auth provider is one of the most expensive mistakes you can make — not because of the monthly bill, but because switching later means rewriting your entire security layer, migrating every user record, and invalidating every active session. It is the kind of decision that feels small on day one and enormous on day three hundred.
In 2026, the landscape has consolidated around four serious options: Supabase Auth, Clerk, Auth.js (formerly NextAuth.js), and Firebase Auth. Each one makes different trade-offs between cost, developer experience, flexibility, and vendor lock-in. This article gives you the real numbers, real code, and a real framework for choosing. No affiliate links. No sponsored opinions. Just data.
Section 1: Why Auth Is Harder Than You Think
If you have ever built a side project, you know the moment. You set up your database, build a couple of pages, and then think: "Alright, I just need to add a login button." You Google "how to add Google login to my app," follow a tutorial, and thirty minutes later you have a working sign-in flow. Done, right?
Not even close. What you built is the visible 10% of authentication. The login button, the OAuth redirect, the welcome screen. The other 90% is invisible, and it is where every auth disaster lives.
The Authentication Iceberg
Session management. After a user logs in, how does your app remember them? Do you store a JWT in a cookie? In localStorage? How long does it last? What happens when it expires — does the user get silently logged out while filling in a long form? Do you use refresh tokens? Where do you store those? If a user logs in from two devices, do both sessions stay active? What if they change their password on one device — should the other session be invalidated?
Token refresh and rotation. JWTs expire. When they do, your app needs to seamlessly request a new one without the user noticing. This means implementing token rotation, handling race conditions when multiple API calls hit at the same time with an expired token, and dealing with the edge case where a refresh token itself has expired. Get this wrong and users randomly get logged out, or worse, their requests silently fail.
Logout. Sounds simple. Delete the cookie. But what about other devices? What about tokens that are already in flight? With JWTs, there is no server-side revocation by default — a token is valid until it expires, even after "logout." You need a token blacklist or short expiry times to handle this properly.
Multi-factor authentication (MFA). Users expect it. Regulators increasingly require it. Implementing TOTP (time-based one-time passwords), SMS codes, or WebAuthn/passkeys is a significant engineering effort. Each method has its own edge cases: what if the user loses their phone? What are the backup codes? How do you handle the enrollment flow?
Rate limiting and brute force protection. Without rate limiting, an attacker can try thousands of passwords per second against your login endpoint. You need progressive delays, account lockouts, CAPTCHA triggers, and IP-based throttling. And you need to do this without locking out legitimate users who just mistyped their password.
Password reset. Email delivery, token expiration, single-use enforcement, and the UI flow for setting a new password. If the reset email does not arrive (spam filters, wrong email), you need a fallback. If the token does not expire, you have a security vulnerability.
Account linking. A user signs up with Google, then later tries to log in with their email/password. Is that the same account? What if they signed up with GitHub using the same email? Account linking logic is notoriously tricky and every provider handles it differently.
The point: Authentication is not a feature. It is a system. The login button is the front door, but behind it is a maze of session management, token lifecycle, security hardening, and edge case handling. This is exactly why auth-as-a-service exists — so you do not have to build the maze yourself. The question is which service builds the maze best for your situation.
Section 2: The Four Options Explained
Let us look at what each provider actually is, how it works architecturally, and what philosophy drives its design.
Supabase Auth
Supabase Auth is the authentication layer of the Supabase platform — an open-source alternative to Firebase. It is built on top of GoTrue, an open-source auth server written in Go, and it integrates directly with a PostgreSQL database.
Architecture: When a user signs in, Supabase Auth issues JWTs. These tokens are then used by the Supabase client library to authenticate requests to your PostgreSQL database. The key differentiator is Row-Level Security (RLS). Instead of writing auth checks in your application code, you write security policies directly in PostgreSQL. The database itself enforces who can read, insert, update, or delete each row. This means your auth logic lives in the database, not scattered across your API endpoints.
What is included: Email/password auth, magic links, phone/SMS auth, and OAuth with Google, GitHub, Apple, Discord, and 15+ other providers. MFA via TOTP is supported. Anonymous sign-in lets users try your app before creating an account. You get a full user management dashboard in the Supabase console.
Philosophy: Supabase believes your database should be the center of your application, and auth is just a gateway to it. If you are already using Supabase for your database, auth is a natural extension. If you are not, adopting Supabase Auth means adopting the entire Supabase stack — which is both its strength and its limitation.
Self-hosting: Fully self-hostable. You can run the entire Supabase stack (including auth) on your own infrastructure using Docker. This is rare among managed auth providers and a major advantage if vendor lock-in concerns you.
- Free tier: 50,000 monthly active users (MAU)
- Complexity: Moderate — requires understanding RLS policies and PostgreSQL
- Vendor lock-in: Low — open source and self-hostable
Clerk
Clerk is a dedicated authentication and user management platform that focuses entirely on developer experience. It does not bundle a database, hosting, or any other infrastructure. It does one thing — auth — and it does it with the most polished developer experience of any option on this list.
Architecture: Clerk runs entirely as a hosted service. When a user signs in, Clerk handles the entire flow on its infrastructure and issues session tokens. Your application verifies these tokens using Clerk's SDK. Clerk provides prebuilt UI components — sign-in forms, sign-up forms, user profile pages, organization switchers — that you drop into your app with a single line of code. These components are beautiful by default and customizable with CSS.
What is included: Email/password, magic links, OAuth with 20+ providers, SMS/phone auth, passkeys and WebAuthn, multi-factor authentication (TOTP and SMS), organizations and roles, invitation flows, webhook support for syncing user data to your own database, and an embeddable user management UI. Clerk also provides impersonation (admins can log in as any user for debugging), device management, and active session listing.
Philosophy: Clerk believes auth should be invisible to the developer. You should not need to think about JWTs, refresh tokens, session management, or security headers. You install the SDK, drop in a component, and it works. The trade-off is that you are fully dependent on Clerk's infrastructure. There is no self-hosting option.
- Free tier: 10,000 MAU
- Paid pricing: $0.02 per MAU after the free tier
- Complexity: Easiest of all four options
- Vendor lock-in: High — proprietary, no self-hosting, all user data lives on Clerk's servers
Auth.js (NextAuth.js v5)
Auth.js is an open-source authentication library for JavaScript applications. It started as NextAuth.js (specifically for Next.js) and has since expanded to support SvelteKit, Express, Nuxt, SolidStart, and other frameworks. It is a library, not a service — it runs entirely within your application.
Architecture: Auth.js runs as middleware in your application. When a user signs in via OAuth, Auth.js handles the redirect flow, exchanges the authorization code for tokens, creates a session, and stores session data in your database (or in a JWT, depending on your configuration). You provide the database adapter (Prisma, Drizzle, TypeORM, MongoDB, or dozens of others), and Auth.js manages the auth tables in your existing database.
What is included: Support for 80+ OAuth providers (Google, GitHub, Apple, Microsoft, Slack, Twitch, and many more), email/password via the Credentials provider, magic links via the Email provider, and database sessions or JWT sessions. Auth.js does not include prebuilt UI components — you build your own login pages. It does not include MFA out of the box, but you can implement it by extending the Credentials provider. It does not include rate limiting, account lockout, or brute force protection — you handle those yourself or add them via middleware.
Philosophy: Auth.js believes authentication should be fully under your control. You own the code, you own the data, you own the infrastructure. Nothing is hidden behind a service. The trade-off is that you are responsible for everything — security hardening, session management, edge cases, and keeping up with security patches. This is the most flexible option and the most labor-intensive option simultaneously.
- Free tier: Completely free, forever (open source, MIT license)
- Complexity: Highest — significant configuration, no prebuilt UI, security features must be added manually
- Vendor lock-in: None — you own everything
Firebase Auth
Firebase Auth is Google's authentication service, part of the broader Firebase platform. It has been around since 2016, making it the most mature option on this list. Millions of applications use it, from small side projects to apps with millions of users.
Architecture: Firebase Auth runs as a fully managed service on Google's infrastructure. When a user signs in, Firebase issues an ID token (a JWT) and a refresh token. The ID token is verified by Firebase's SDKs on the client side and by the Firebase Admin SDK on your server. If you use other Firebase services (Firestore, Cloud Functions, Cloud Storage), the ID token automatically controls access through Firebase Security Rules — similar in concept to Supabase's RLS but using a different rule language.
What is included: Email/password, phone/SMS auth (a standout feature — Firebase's phone auth is the most battle-tested in the industry), anonymous auth (users get a temporary account that can be upgraded to a permanent one later), OAuth with Google, Apple, Facebook, Twitter, GitHub, Microsoft, and Yahoo, magic links, multi-factor authentication via SMS and TOTP, and a custom auth system that lets you integrate with any identity provider by minting your own tokens. Firebase also includes App Check, which verifies that requests come from your legitimate app and not from a script or spoofed client.
Philosophy: Firebase believes in a fully integrated platform. Auth is designed to work seamlessly with Firestore (database), Cloud Functions (serverless compute), Cloud Storage (file uploads), and Firebase Hosting. If you are building within the Google ecosystem, everything connects automatically. If you are not, Firebase Auth still works as a standalone service, but you lose much of its magic.
- Free tier: 50,000 MAU (email/password and social login), 3,000 MAU for phone/SMS
- Paid pricing: $0.0055 per MAU after the free tier (email/social), $0.01 per SMS verification
- Complexity: Moderate — well-documented, good SDKs, but the Firebase console can be overwhelming
- Vendor lock-in: Moderate-high — deeply integrated with Google Cloud, difficult (but not impossible) to migrate away
Quick summary: Supabase Auth is best if you want open-source and database-centric auth. Clerk is best if you want the easiest setup and the best UI components. Auth.js is best if you want total control and zero vendor lock-in. Firebase Auth is best if you are building within the Google ecosystem or need production-grade phone authentication.
Section 3: Pricing at Scale
Pricing only matters when you grow. At 100 users, everything is free. At 100,000 users, the differences are significant. Here is what you actually pay at each scale.
Monthly Cost by MAU
| MAU | Supabase Auth | Clerk | Auth.js | Firebase Auth |
|---|---|---|---|---|
| 100 | $0 | $0 | $0 | $0 |
| 1,000 | $0 | $0 | $0 | $0 |
| 10,000 | $0 | $0 | $0 | $0 |
| 50,000 | $0 | $800/mo | $0 | $0 |
| 100,000 | $25/mo * | $1,800/mo | $0 | $275/mo |
* Supabase note: Supabase Auth itself is free up to 50K MAU on the free plan. Beyond that, you need the Pro plan ($25/month), which includes 100K MAU. The $25 covers the entire Supabase platform (database, storage, auth), not just auth alone. Additional MAU beyond 100K is billed at $0.00325 per MAU.
How the Numbers Break Down
Clerk is the most expensive at scale. The 10,000 MAU free tier is generous for startups, but at $0.02 per MAU beyond that, costs rise quickly. At 50,000 MAU, you are paying $800/month (40,000 paid MAU times $0.02). At 100,000 MAU, that is $1,800/month (90,000 paid MAU times $0.02). For a venture-funded SaaS company, this is a rounding error. For a bootstrapped project or a student app that goes viral, it is a real constraint.
Firebase Auth is remarkably cheap. At $0.0055 per MAU beyond the 50K free tier, 100,000 MAU costs just $275/month. Phone/SMS auth is more expensive ($0.01 per verification), so if you rely heavily on phone auth, factor that in separately.
Auth.js is free forever because it runs on your infrastructure. However, "free" is misleading. You pay for the server that runs it, the database that stores sessions, and the developer time to maintain it. For a student running a $5/month VPS, the infrastructure cost is negligible. For a team that values engineering time at $150/hour, the "free" library that requires 40 hours of security hardening is not actually free.
Supabase Auth hits the sweet spot for most projects. The 50K MAU free tier is enormous, and even on the Pro plan, you get 100K MAU for $25/month — a fraction of what Clerk charges. The catch is that you are adopting the Supabase platform, not just auth. If you were already planning to use PostgreSQL, this is a bonus. If you wanted MongoDB or a different database, Supabase Auth may not be the right fit.
Hidden costs to watch for: Clerk charges for MAU, which means a user who logs in once in January and once in February counts as a MAU both months. Firebase counts phone verifications separately from MAU — at scale, SMS costs can exceed auth costs. Supabase's free tier has limits on database size (500MB) and bandwidth that may force an upgrade before you hit the auth limit. Auth.js has zero service costs but potentially significant labor costs.
Session Management Approaches: JWT vs Database Sessions
How each provider handles sessions affects both performance and security.
Supabase Auth uses JWTs by default. Tokens are issued on login and verified statelessly. Refresh tokens are stored in the database. JWTs expire after 1 hour by default (configurable). This is fast — no database query needed to verify a request — but means you cannot instantly revoke a session. You must wait for the JWT to expire.
Clerk uses short-lived session tokens (similar to JWTs) combined with a server-side session registry. This gives you both the performance of stateless tokens and the ability to revoke sessions instantly. Clerk automatically handles token refresh in the background. This is the most sophisticated session management of the four, and it is completely invisible to the developer.
Auth.js supports both JWT sessions and database sessions. With JWT sessions, behavior is similar to Supabase — fast verification, no instant revocation. With database sessions, every request hits the database to verify the session, but you can revoke any session instantly by deleting its row. The choice is yours, and this flexibility is a strength of Auth.js.
Firebase Auth uses ID tokens (JWTs) that expire after 1 hour and refresh tokens that last indefinitely (until explicitly revoked). Firebase's SDK handles refresh automatically on the client side. Server-side, you can revoke refresh tokens, but any ID tokens already issued remain valid until they expire. Firebase recommends checking token revocation status for sensitive operations.
Section 4: Implementation Complexity
Theory is one thing. Let us look at what it actually takes to implement Google OAuth sign-in with each provider. These examples use Next.js as the framework because it is the most common choice in 2026, but the relative complexity holds across frameworks.
Clerk: ~15 lines of code
// 2. Add CLERK_SECRET_KEY and NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY to .env
// 3. Add middleware (middleware.ts):
import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
// 4. Wrap your app (layout.tsx):
import { ClerkProvider, SignInButton, UserButton } from "@clerk/nextjs";
export default function Layout({ children }) {
return (
<ClerkProvider>
<SignInButton />
<UserButton />
{children}
</ClerkProvider>
);
}
// That is it. Google OAuth, session management, user UI — all handled.
Clerk requires the least code because it provides prebuilt components. The SignInButton renders a complete OAuth modal. The UserButton shows the user's avatar with a dropdown for profile, sign out, and session management. You configure which OAuth providers to enable in the Clerk dashboard, not in code.
Firebase Auth: ~30 lines of code
// 2. Create a Firebase project and enable Google sign-in in the console
// 3. Initialize Firebase (lib/firebase.ts):
import { initializeApp } from "firebase/app";
import { getAuth, GoogleAuthProvider, signInWithPopup } from "firebase/auth";
const app = initializeApp({
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
});
export const auth = getAuth(app);
export const googleProvider = new GoogleAuthProvider();
// 4. Sign-in function (components/LoginButton.tsx):
import { auth, googleProvider } from "@/lib/firebase";
import { signInWithPopup } from "firebase/auth";
export function LoginButton() {
const handleLogin = async () => {
const result = await signInWithPopup(auth, googleProvider);
const user = result.user;
console.log("Logged in:", user.email);
};
return <button onClick={handleLogin}>Sign in with Google</button>;
}
Firebase requires more setup because you need to initialize the Firebase app with configuration values, create a provider instance, and call the sign-in function. But it is still straightforward. Session management is handled automatically by Firebase's SDK — the auth.onAuthStateChanged listener tracks the user's state, and tokens refresh in the background.
Supabase Auth: ~25 lines of code
// 2. Create a Supabase project and enable Google OAuth in the dashboard
// 3. Initialize Supabase (lib/supabase.ts):
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
// 4. Sign-in function (components/LoginButton.tsx):
import { supabase } from "@/lib/supabase";
export function LoginButton() {
const handleLogin = async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: { redirectTo: window.location.origin + "/dashboard" },
});
};
return <button onClick={handleLogin}>Sign in with Google</button>;
}
// 5. Check session (middleware or page):
const { data: { session } } = await supabase.auth.getSession();
Supabase is comparable to Firebase in complexity. The main difference is that Supabase uses redirect-based OAuth by default (the user is redirected to Google and back), while Firebase uses a popup. Both approaches work, but you need to handle the callback URL on the Supabase side. Supabase also requires setting up RLS policies if you want database-level security, which adds complexity beyond the basic auth setup.
Auth.js: ~60 lines of code
// 2. Create auth config (auth.ts):
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async session({ session, user }) {
session.user.id = user.id;
return session;
},
},
});
// 3. Create API route (app/api/auth/[...nextauth]/route.ts):
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
// 4. Create middleware (middleware.ts):
export { auth as middleware } from "@/auth";
export const config = { matcher: ["/dashboard/:path*"] };
// 5. Set up Prisma schema (add User, Account, Session tables)
// 6. Run: npx prisma migrate dev
// 7. Create Google OAuth credentials in Google Cloud Console
// 8. Set callback URL: http://localhost:3000/api/auth/callback/google
// 9. Build your own sign-in page (app/login/page.tsx):
import { signIn } from "@/auth";
export default function LoginPage() {
return (
<form action={async () => { "use server"; await signIn("google"); }}>
<button type="submit">Sign in with Google</button>
</form>
);
}
Auth.js requires the most setup. You need to configure the provider, set up a database adapter, create the API route handler, add middleware for protected routes, set up the Prisma schema with the correct tables (User, Account, Session, VerificationToken), run a database migration, create OAuth credentials in Google Cloud Console, configure the callback URL, and build your own sign-in page from scratch. Each step is well-documented, but there are many steps, and each one is a potential point of failure.
MFA/2FA Support
| Feature | Supabase | Clerk | Auth.js | Firebase |
|---|---|---|---|---|
| TOTP (Authenticator App) | Yes | Yes | Manual | Yes |
| SMS Codes | Yes | Yes | Manual | Yes |
| Passkeys / WebAuthn | No | Yes | Via plugin | No |
| Backup Codes | Yes | Yes | Manual | No |
| Enrollment UI | Dashboard | Prebuilt | Build yourself | SDK helpers |
"Manual" for Auth.js means you can implement it, but there is no built-in support. You write the TOTP verification logic, the enrollment flow, and the recovery mechanism yourself. For experienced developers, this is doable. For students or small teams, it is a significant time investment with real security implications if done wrong.
Social Login Provider Count
| Provider | OAuth Providers | Notable Inclusions |
|---|---|---|
| Supabase | 18+ | Google, GitHub, Apple, Discord, Slack, Twitch, Spotify |
| Clerk | 20+ | Google, GitHub, Apple, Microsoft, LinkedIn, Notion, HubSpot |
| Auth.js | 80+ | Everything above plus dozens of niche providers, custom OIDC |
| Firebase | 8 built-in | Google, Apple, Facebook, Twitter, GitHub, Microsoft, Yahoo, custom OIDC |
Auth.js dominates in provider count because the community contributes new providers constantly. If you need to authenticate with an obscure enterprise SAML/OIDC identity provider, Auth.js is your best bet. Firebase has the fewest built-in providers, but its custom auth feature lets you integrate with any identity provider by writing a Cloud Function that mints Firebase tokens.
Migration Difficulty
The hardest question: what happens if you choose wrong and need to switch?
Migrating away from Clerk is the hardest. All user data, passwords (hashed), sessions, and organization structures live on Clerk's servers. Clerk provides a user export API, but you get email addresses and metadata — not password hashes. Every user who signed up with email/password must reset their password after migration. OAuth users can be re-linked, but it requires careful mapping. Organizations and role assignments must be rebuilt manually in your new system.
Migrating away from Firebase Auth is moderately difficult. Firebase allows exporting user records including password hashes (using a Firebase-specific scrypt variant). You can import these into another system, but you need to implement the same hashing algorithm. OAuth tokens cannot be transferred. The Firebase Admin SDK provides a listUsers method for bulk export.
Migrating away from Supabase Auth is easier because your user data lives in a PostgreSQL database that you control (even on the hosted plan). You can directly query the auth.users table, export the data, and import it into another system. Password hashes use bcrypt, which is standard and supported by virtually every auth system. If you self-hosted Supabase, migration is even simpler — you already have the database.
Migrating away from Auth.js is the easiest because there is nothing to migrate away from. Your user data is in your database, in your schema, under your control. You just stop using the Auth.js library and implement auth another way. The data stays exactly where it is.
Section 5: The Decision Framework
Choosing an auth provider is not about which one is "best." It is about which one fits your specific situation. Here is a decision framework based on the most common scenarios.
Use Clerk if...
- You are building a SaaS product and need organizations, roles, and invitations out of the box.
- You want to launch as fast as possible and do not want to think about auth at all.
- You value developer experience above all else and are willing to pay for it.
- Your projected user count stays under 10,000 MAU (free tier) or your business model supports the per-MAU cost.
- You are comfortable with full vendor dependency and do not anticipate needing to self-host.
Clerk is ideal for: Funded startups, SaaS MVPs, teams that prioritize shipping speed over infrastructure control. If your pitch is "we launched in two weeks," Clerk is how you do it.
Use Supabase Auth if...
- You are already using (or planning to use) PostgreSQL as your database.
- You want Row-Level Security to enforce authorization at the database level.
- You care about open source and want the option to self-host everything.
- You need a generous free tier (50K MAU) and low costs at scale.
- You are building a real-time application that benefits from Supabase's real-time subscriptions.
Supabase is ideal for: Full-stack applications where the database is the core, indie hackers and bootstrapped projects that need professional auth without professional pricing, and teams that want an exit strategy (self-hosting) if the managed service becomes too expensive or changes its terms.
Use Auth.js if...
- You need to support an unusual or enterprise OAuth provider not available on other platforms.
- You already have an existing database and do not want to adopt a new platform.
- Vendor lock-in is a hard no — your organization requires full control over auth infrastructure.
- You are an experienced developer comfortable with security implementation.
- You are building an open-source project and need an auth solution with no service dependency.
- Budget is zero and must stay zero, including at scale.
Auth.js is ideal for: Experienced developers, open-source projects, enterprise environments with strict vendor policies, and anyone who would rather spend time than money. It is also the right choice when you need auth in a non-standard framework or a custom server setup where managed services do not integrate cleanly.
Use Firebase Auth if...
- You are building a mobile app (Android or iOS) — Firebase's mobile SDKs are the most mature.
- You need phone/SMS authentication that works globally and reliably.
- You are already using other Firebase or Google Cloud services (Firestore, Cloud Functions).
- You need anonymous auth to let users try your app before signing up.
- You want a proven, battle-tested system used by millions of apps over nearly a decade.
Firebase is ideal for: Mobile-first applications, apps that need phone verification, projects already in the Google ecosystem, and teams that value a decade of production reliability over cutting-edge features.
The Quick Decision Flowchart
Answer these questions in order:
- Are you already using Supabase or PostgreSQL? → Use Supabase Auth. The integration is seamless and you get RLS for free.
- Are you already using Firebase, Firestore, or Google Cloud? → Use Firebase Auth. Fighting the ecosystem is not worth it.
- Do you need to ship in under a week and organizations/roles are required? → Use Clerk. Nothing else matches its speed for multi-tenant apps.
- Do you need 80+ OAuth providers or have zero budget including at scale? → Use Auth.js. It is the only option that is truly free at any scale with the broadest provider support.
- None of the above? → Start with Supabase Auth. It offers the best balance of cost, flexibility, and escape hatch (self-hosting). You can always migrate later, and migrating from Supabase is easier than migrating from any other option on this list.
What We Use at NETWORKERS HOME
For the Student Portal you are reading this on, we use Auth.js with Prisma and PostgreSQL. We made this choice because we already had a PostgreSQL database, we needed full control over the session management and user model, and we wanted zero ongoing service costs as the student body grows. The initial setup took longer than a managed service would have, but we own every piece of the auth system and can modify it freely. For a student learning platform, this trade-off made sense.
Your project is different. The right choice depends on your constraints — time, money, technical ability, and how much control you need. Use the framework above, pick the option that fits, and do not look back. The worst auth decision is the one you keep second-guessing instead of building features.