Resources · Platform Building Blocks

Authentication

How to add login to your MVP without rolling your own crypto, leaking sessions, or burning a weekend on OAuth callbacks.

Auth is the part of an MVP that punishes overthinking. You don't need SAML, you don't need a custom session store, and you definitely don't need to hash passwords yourself. You need a working login that doesn't embarrass you in front of your first 100 users. Here's how we pick.

Auth.js v5 (NextAuth)

Auth.js is the rename of NextAuth. v5 dropped the [...nextauth] route gymnastics, added first-class App Router support, and made the Drizzle adapter actually pleasant. It's MIT-licensed, runs entirely on your own infrastructure, and supports magic links, OAuth, credentials, and (with a third-party adapter) WebAuthn.

The reason this site uses Auth.js v5 is simple: a magic-link flow with Resend is about 60 lines of code, costs $0 until you cross 3,000 emails per month, and the user data lives in the same Postgres or SQLite that the rest of the app uses. No vendor on the critical path of "can my user log in."

// auth.ts
import NextAuth from "next-auth"
import Resend from "next-auth/providers/resend"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { db } from "@/db"

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [
    Resend({ from: "auth@yourdomain.com" }),
  ],
})

The flip side: you own the database tables, the cron job that prunes expired sessions, and the email template. For an MVP that's a feature, not a bug.

Clerk

Clerk is the "I have $30 a month and four hours of life" answer. Drop in <SignIn />, get a working component with email, OAuth, MFA, and now passkeys, and walk away. Their dashboard is genuinely good. Their pricing in 2026 is free up to 10,000 monthly active users, then $25/mo for the Pro plan plus $0.02 per MAU over the included quota.

The trade you're making: user records live in Clerk's database, not yours. Joining users to orders means either syncing via webhooks or storing the Clerk user ID as a foreign key and hoping their API never has a bad afternoon. For B2B SaaS where login is a small part of the product, that's a totally fine trade. For a social product where the user IS the product, you'll outgrow it.

Supabase Auth

If you're already using Supabase for your database, their auth is right there and free up to 50,000 MAU. It speaks Postgres row-level security, which is the closest thing to a superpower in the Supabase ecosystem. The downside is that switching auth later means migrating both your database and your auth at the same time, which is a worse problem than either alone.

Lucia

Lucia in early 2024 was the darling of "I want auth without a framework." In 2024 the maintainer announced Lucia v3 would be the last released version and pivoted the project into a learning resource — lucia-auth.com is now a tutorial site rather than a library. You can still copy the patterns, but don't npm install your way into a deprecated dependency for an MVP.

Firebase Auth

Google's auth product. Free for unlimited email/password and OAuth users, paid only when you turn on phone auth or the Identity Platform tier. It works. The UI is dated, the SDK is heavy, and the lock-in is real because Firebase Auth tokens are tied to Firebase projects. We'd reach for it only if the rest of the app already lives in Firebase.

WorkOS

WorkOS is what you add when an enterprise customer says the word "SAML." Their AuthKit hosted UI is free up to 1 million MAU for social and email auth, and SSO/SCIM are priced per connection (roughly $125 per enterprise org per month). You should not reach for WorkOS to ship your MVP. You should bookmark it for the day a customer with a procurement team shows up.

Method UX Implementation cost Recovery story
Magic link One click in email Low (provider does it) Resend the link
OAuth (Google, GitHub) One click in browser Medium (callback URLs, scopes) Re-auth with provider
Password Two fields High (reset flow, breach checks, hashing) Forgot-password email
Passkey Touch ID / Face ID Medium (WebAuthn, fallback flow) Re-register on new device

Magic links are the lowest-friction option that actually works in 2026. Gmail, Outlook, and Yahoo all deliver them reliably if your domain is set up right (see /resources/email/). The only real downside is the round-trip to the inbox, which feels slow on mobile.

OAuth is mandatory if your users live on GitHub (devtools), Google (consumer), or Microsoft (enterprise). Add it as a second option, not a primary one — the primary should be something that works for users without those accounts.

Passwords are a tax. If you must have them, use Auth.js or Clerk's built-in password handling and never write bcrypt.hash in your own code.

Passkeys are real, well-supported on iOS 17+, Android 14+, and recent macOS / Windows builds, and adoption is still under 10% of consumer logins. Offer them as an option for power users. Don't make your grandmother register one to read your beta.

Comparison

Service Free tier OSS? Hosted UI Magic link OAuth providers Wiring effort
Auth.js v5 Unlimited (your infra) Yes (MIT) No (you build) Yes (Resend, others) 80+ built in Medium
Clerk 10,000 MAU No Yes Yes 20+ Very low
Supabase Auth 50,000 MAU Partially Pre-built components Yes 15+ Low
Lucia N/A (deprecated) Yes No DIY DIY High
Firebase Auth Unlimited (basic) No FirebaseUI Yes 10+ Low
WorkOS AuthKit 1M MAU (social/email) No Yes Yes 20+ + SSO Low

Anti-patterns we keep seeing

Don't roll your own session storage. The temptation is real — sessions are "just" a cookie pointing at a row in a database — and the failure modes are a horror show. Cookie not set as Secure, HttpOnly, and SameSite=Lax? Welcome to CSRF. Session ID generated with Math.random()? Welcome to session prediction attacks. Use the session table your auth library ships with.

Don't write bcrypt.hash from scratch in 2026. Argon2id is the recommended algorithm and the libraries that wrap it correctly are fewer than you think. If you must store passwords, let Auth.js, Clerk, or Supabase do it.

Don't put password hashes in localStorage. Yes, this still happens. Yes, we have read the code. The browser is not a vault. Tokens (preferably short-lived JWTs or session IDs) go in HttpOnly cookies. Hashes go in the database. Plaintext passwords go nowhere.

Don't email a password reset link that doesn't expire. 15 minutes is plenty. 24 hours is the absolute outside.

Don't trust email from an OAuth provider as proof of identity unless the provider explicitly marks it as verified. GitHub will happily send you an unverified email; treating it as verified is how account takeovers happen.

Our recommendation

For a typical MVP shipping in 2026, use Auth.js v5 with Resend magic links. It's free up to 3,000 emails per month on Resend, the user table lives in your own database (Drizzle + libsql/Turso, see /resources/databases/), and the entire flow fits in one file. When you need OAuth, add Google as a second provider. When you need passkeys, add the WebAuthn adapter. You will not regret this stack.

If your time is more valuable than $25/mo and you want to skip even that much wiring, use Clerk. It's the right answer for solo founders who are not enjoying the auth chapter.

If you have an enterprise pilot with a Fortune 500 logo on the line and the word "SSO" in a contract, add WorkOS alongside whatever you already have. Don't replace, augment.