Build a Startup Cheatsheet

Auth and OAuth

Use this Build a Startup reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Why Auth Is Harder Than It Looks

Authentication is one of the most deceptively complex parts of any product. Getting it wrong means security vulnerabilities, user lockouts, or credential leaks. Getting it right from scratch takes weeks and requires deep knowledge of JWTs, session management, CSRF protection, OAuth flows, and token refresh logic.

The practical recommendation: use an auth library or service unless auth is your competitive differentiation (which it almost never is).

Core Concepts

Before choosing a tool, understand the building blocks:

Authentication — proving who you are ("I am Alice").

Authorization — what you are allowed to do ("Alice can edit this post").

Session — a server-side record that a user is logged in. Stored on the server, referenced via a cookie.

JWT (JSON Web Token) — a signed, self-contained token that encodes identity claims. The server signs it; clients present it with every request. No server-side state required, but harder to invalidate.

OAuth 2.0 — a protocol that lets users authorize your app to act on their behalf at a third-party service (Google, GitHub, etc.). Commonly used for "Sign in with Google".

OIDC (OpenID Connect) — an identity layer on top of OAuth 2.0. When you "Sign in with Google", OIDC is what actually gives you the user's profile information.

Cookie vs. Authorization header

  • httpOnly cookies — browser sends them automatically; JavaScript cannot read them; best for web apps; CSRF-safe when combined with SameSite=Lax
  • Authorization: Bearer <token> header — JavaScript sets it manually; required for mobile/API clients; no CSRF risk but exposed to XSS

Option 2: Supabase Auth

Supabase includes a full auth system. If you are already using Supabase for your database, Supabase Auth is the natural choice.

Free tier: 50,000 monthly active users.

Pros: - Included with Supabase at no extra cost - Email/password, magic links (passwordless), OAuth, phone/OTP - JWTs that integrate directly with Row-Level Security policies - User table is in your Postgres database — queryable with standard SQL - Social providers: Google, GitHub, Apple, Discord, Twitter, and more

Cons: - UI components are more basic than Clerk's — you build your own sign-in form - Session refresh logic requires setup - Less granular user management dashboard than Clerk

import { createClient } from "@supabase/supabase-js";
const supabase = createClient(url, key);

// Sign up
await supabase.auth.signUp({ email, password });

// Sign in with Google OAuth
await supabase.auth.signInWithOAuth({ provider: "google" });

// Get current user
const { data: { user } } = await supabase.auth.getUser();

Option 3: Auth.js (formerly NextAuth)

Auth.js is an open-source auth library for Next.js (and other frameworks). You install it, configure providers, and it handles OAuth flows, session cookies, and callbacks.

Cost: Free (open-source).

Pros: - Free and open-source; no vendor lock-in - Works with any Postgres, MySQL, or MongoDB adapter - 50+ OAuth providers pre-configured (Google, GitHub, Spotify, etc.) - Full control over session behavior

Cons: - More configuration required than Clerk or Supabase Auth - No hosted UI — you build your own sign-in page - Documentation and API changed significantly between v4 and v5

// auth.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [Google],
  callbacks: {
    session({ session, token }) {
      session.user.id = token.sub!;
      return session;
    },
  },
});

Option 4: Hand-Rolled Auth (When It Makes Sense)

Building auth yourself is the right choice when:

  • You have very specific requirements (e.g., invite-only sign-up, custom application flow)
  • Your team has security expertise and time to invest
  • You want zero external auth dependencies

A minimal secure implementation requires:

  1. Password hashing with bcrypt or argon2 — never store plaintext passwords
  2. httpOnly, Secure, SameSite=Lax session cookies
  3. Crypto-random session tokens (not predictable IDs)
  4. Rate limiting on login endpoints
  5. CSRF protection
  6. Secure password reset flow (time-limited, single-use tokens)
import bcrypt from "bcryptjs";
import { randomBytes } from "crypto";

// Hash on sign-up
const hash = await bcrypt.hash(password, 12);

// Verify on login
const valid = await bcrypt.compare(password, storedHash);

// Generate a session token
const token = randomBytes(32).toString("hex");

OAuth: "Sign in with Google" Flow

OAuth is a delegation protocol, not an authentication protocol — but OIDC layers identity on top. Here is what happens when a user clicks "Sign in with Google":

  1. Your app redirects the user to Google's authorization server with your client_id, requested scopes, and a redirect_uri
  2. The user approves at Google
  3. Google redirects back to your redirect_uri with an authorization code
  4. Your server exchanges the code for an access_token and id_token (server-to-server, never exposed to the browser)
  5. You verify the id_token, extract the user's email and Google ID, and create or look up their account

Auth.js, Clerk, and Supabase Auth handle all of this for you. You only need the client_id and client_secret from the provider's developer console.

Getting Google OAuth credentials: 1. Go to console.cloud.google.com 2. Create a project → APIs & Services → Credentials → OAuth 2.0 Client ID 3. Set Authorized redirect URIs to http://localhost:3000/api/auth/callback/google (dev) and your production URL

Comparing Auth Options

ClerkSupabase AuthAuth.jsHand-rolled
Setup time30 min1 hour2–3 hoursDays
Cost$25/mo after 10K MAUFree (bundled)FreeFree
UI componentsBuilt-inBasicNoneBuild yourself
OAuth providers30+20+50+Manual
User dashboardExcellentGoodNoneBuild yourself
Vendor lock-inHighMediumLowNone
FlexibilityLowMediumHighFull
Best forSpeed, great UXSupabase usersCustom needsUnique requirements

Authorization: Roles and Permissions

Authentication tells you who the user is. Authorization tells you what they can do.

Simple role system (most startups): add a role column (user, admin, moderator) to the users table. Check it in every protected route.

const user = await getUser(session.userId);
if (user.role !== "admin") return new Response("Forbidden", { status: 403 });

Row-level security (Supabase): write policies in Postgres that enforce data access at the database level — no need to check permissions in every API route.

-- Users can only see their own data
CREATE POLICY "Users see own rows" ON posts
  FOR SELECT USING (auth.uid() = user_id);

Attribute-based access control (ABAC): for complex permission systems (SaaS with teams, workspaces, and custom roles), consider Permit.io or Cerbos — dedicated authorization engines that keep policy out of application code.

Security Checklist for Auth

  • Passwords hashed with bcrypt/argon2 (cost factor ≥10)
  • Session cookies: httpOnly, Secure, SameSite=Lax, short max-age (7–30 days)
  • Rate limit login attempts (5–10 per IP per 15 minutes)
  • Email verification before account activation
  • Secure password reset: time-limited (15–60 min), single-use, invalidated on use
  • Logout invalidates the session server-side
  • No sensitive user data in JWT payloads (JWTs are base64, not encrypted)
  • HTTPS everywhere — never send credentials over HTTP