Chapter 8: SAAS Authentication: Auth Solution Comparison
In Chapter 7, we chose the ORM path, deciding to use Drizzle to control our database. This decision directly impacts our Authentication solution.
Chapter 8: SAAS Authentication: Auth Solution Comparison
In Chapter 7, we chose the ORM path, deciding to use Drizzle to control our database. This decision directly impacts our Authentication solution.
If you're used to Django Admin and django.contrib.auth in the Python world, you'll love how it handles everything for you: user models, sessions, password hashing, permissions. If you use Flask, you might combine Flask-Login and SQLAlchemy.
In the Next.js ecosystem, authentication is also a crossroads requiring tradeoffs, and it's closely tied to your database choice from the previous chapter.
8.1. Path A (BaaS): Supabase Auth
If we chose the Supabase platform, the authentication solution is almost automatically determined: use Supabase Auth.
8.1.1. Advantages: Perfect Integration with RLS, Works Out of the Box
Supabase Auth is not a standalone library, it's a core feature of the Supabase platform.
-
Works Out of the Box: It provides complete user management UI, OAuth (Google, GitHub...), email/password, magic links, and all other features.
-
Perfect Integration with RLS: This is its biggest killer feature. Remember the RLS policy from Chapter 7?
-- Supabase RLS Policy CREATE POLICY "Users can only see their own projects" ON projects FOR SELECT USING ( auth.uid() = owner_id );The
auth.uid()function here is automatically injected into the database context by Supabase Auth. When a user logs in through the Supabase client, Supabase generates a JWT. When this JWT is sent to the database, PostgreSQL can recognize it and know "who the current user is."This deep binding of authentication and database security policies is the core advantage of the Supabase model. Your application-layer code barely needs to write any permission checks, because the database handles it.
Disadvantage: Obviously, you're deeply locked into the Supabase platform. Your users table, authentication logic, and security model are all controlled by Supabase.
8.2. Path B (ORM): better-auth - [This Book's Project Choice]
Since we chose Drizzle (ORM path), we've given up Supabase's integrated package. Therefore, we need a standalone, self-hostable authentication library that must work perfectly with our Drizzle Schema.
In the Next.js ecosystem, the most famous standalone authentication library is Auth.js (formerly NextAuth.js).
better-auth (a hypothetical library used in our project, typically based on Auth.js V5 or similar concepts) represents the evolution of this pattern: a framework-agnostic, highly customizable authentication solution that can adapt to any ORM.
8.2.1. Advantages: Drizzle Compatible, Highly Customizable
The reasons for choosing better-auth (or Auth.js) are exactly the same as our reasons for choosing Drizzle: complete control.
- Database Control:
better-authdoes not own your user table. It provides authentication logic, while data storage is handed to you through an "adapter." Our实战项目 uses theDrizzleAdapter. - Schema Belongs to You: This means the
userstable (andsessions,accounts, etc.) are entirely defined by us insrc/db/schema.ts(like in Chapter 7). We can freely add fields to theuserstable (likecredits,stripeCustomerId), andbetter-authwon't interfere. - Highly Customizable: You have complete control over login flows, session strategies (JWT vs. Database Sessions), OAuth callbacks, and everything else.
- Framework Agnostic: While it works best in Next.js, it's not bound to Next.js itself.
8.2.2. [Code Analysis]: Analyzing src/lib/auth.ts Implementation
In our SAAS template project, src/lib/auth.ts (or src/auth.ts) is the "heart" of the authentication system. It replaces all AUTH-related configuration in Django's settings.py.
Let's conceptually analyze the structure of this file:
// src/lib/auth.ts
import NextAuth from "next-auth";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import Google from "next-auth/providers/google";
// ... other providers like GitHub ...
import { db } from "@/db"; // Import our Drizzle instance
import { usersTable, accountsTable, sessionsTable } from "@/db/schema"; // Import Drizzle schema
// 1. NextAuth() is the core configuration function
export const {
handlers, // Export API route handlers (e.g., /api/auth/...)
auth, // Export session getter (used in RSC and Server Actions)
signIn, // Export sign-in function
signOut, // Export sign-out function
} = NextAuth({
// 2. [Core] Adapter
// Tell Auth.js how to communicate with our Drizzle database
adapter: DrizzleAdapter(db, {
usersTable,
accountsTable,
sessionsTable,
// ... other tables ...
}),
// 3. Authentication Providers
// Configure the login methods we support
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
// GitHub({...}),
// Resend({...}), // Magic links/email login
],
// 4. Session Strategy
// We choose "database" strategy, not "jwt"
// This is closer to Django's session model, more secure, and easy for server-side queries
session: {
strategy: "database",
},
// 5. Callbacks
// This is the most critical part for customization
callbacks: {
// [Important] When session is queried
// Default session only contains email/name/image
// We need to inject 'id' and 'credits' into the session
async session({ session, user }) {
if (session.user) {
session.user.id = user.id; // user.id comes from Drizzle adapter
// (Concept) Get additional data from user object
// session.user.credits = user.credits;
}
return session;
},
// [Important] When user logs in or registers (JWT callback)
// Can handle new user registration logic here, like allocating initial credits
async jwt({ token, user, trigger }) {
if (trigger === "signUp" && user) {
// (Concept) Call Server Action to allocate credits on new user registration
// await assignInitialCredits(user.id);
}
return token;
},
},
// 6. (Optional) Custom login pages
pages: {
signIn: "/login",
// error: "/auth-error",
},
});Key Points:
DrizzleAdapteris the bridge connectingbetter-authlogic andDrizzledatabase.sessioncallback is crucial. We must use it to adduser.idto thesessionobject, so our Server Components and Server Actions can get the current logged-in user's ID through theauth()function.- Control: We fully control both the data model (Drizzle) and authentication logic (Callbacks).
8.3. [Skill Practice]: Exploring Client-side Cookie Secure Session Management
This Skill (nextjs-client-cookie-pattern/SKILL.md) explores a common SAAS scenario: users have non-sensitive preferences on the client side, such as "sidebar collapsed," "last visited project ID," or "selected theme (dark/light)."
Problem: Where should we store this state?
- Zustand (client state): Works, but data is lost when user refreshes the page.
- localStorage: Works, but it's inaccessible on the server side (RSC). If you want the server to know user preferences when rendering pages (e.g., correctly rendering dark mode),
localStoragecan't do it. - Database: Works, but calling the database (
await db.update(...)) for something as trivial as "collapse sidebar" is too expensive.
Best Solution: Client-side Cookies
better-auth uses secure, HttpOnly cookies to manage sessions. But we can use non-HttpOnly, client-readable/writable cookies to manage user preferences.
Skill Core Practices:
-
Use
js-cookielibrary: This is a standard library for reading/writing cookies on the client side.// src/components/sidebar.tsx "use client"; import Cookies from "js-cookie"; import { useState, useEffect } from "react"; export function Sidebar() { // 1. Initialize state from cookie const [isCollapsed, setIsCollapsed] = useState(() => { const saved = Cookies.get("sidebar-collapsed"); return saved === "true"; }); // 2. Write to cookie when state changes useEffect(() => { Cookies.set("sidebar-collapsed", String(isCollapsed), { expires: 365 }); }, [isCollapsed]); const toggle = () => setIsCollapsed(prev => !prev); // ... JSX ... } -
Read preferences in RSC: Next.js provides the
cookies()function (fromnext/headers), which allows Server Components to read cookies sent by the browser.// src/app/[locale]/layout.tsx (Server Component) import { cookies } from "next/headers"; import { Sidebar } from "@/components/sidebar"; export default function AppLayout({ children }) { // 3. Read cookie on the server! const cookieStore = cookies(); const initialCollapsed = cookieStore.get("sidebar-collapsed")?.value === "true"; return ( <div className="flex"> {/* 4. Pass server-read value to client component */} {/* This avoids hydration errors from client-server state mismatch */} <Sidebar initialCollapsed={initialCollapsed} /> <main>{children}</main> </div> ); }(The client component
Sidebarneeds to be modified accordingly to accept theinitialCollapsedprop)
Conclusion: This pattern is very powerful. It uses cookies as a bridge to achieve:
- Client-side fast read/write and persistence (
js-cookie). - Server-side (RSC) accessibility (
next/headers). - Avoids unnecessary database requests.
This complements our choice of better-auth's "database session" strategy (for security) and "client-side cookies" (for preferences), forming a robust, high-performance session management pattern in SAAS applications.
Categories
src/lib/auth.ts Implementation8.3. [Skill Practice]: Exploring Client-side Cookie Secure Session ManagementMore Posts
Chapter 3: React - Declarative UI (Goodbye Jinja2)
As a Python backend developer, you're probably most familiar with Jinja2. In Flask or Django, you fetch data from the database, 'inject' it into an HTML template, the server 'renders' an HTML string, and sends it to the browser. This process is imperative - you tell the template engine 'loop here, insert this variable here'...
Chapter 10: Database Migrations and Operations
In Chapters 7 and 9, we defined our database table structure in the src/db/schema/ directory. But we left a critical question: when you modify the schema (like adding a bio field to usersTable), how do you safely apply this change to a production database that's already running live?
Chapter 12: SAAS Pricing: Credits and Metering System
In Chapter 11, we successfully integrated Stripe Checkout and Webhooks, establishing the 'subscription' foundation for our SAAS. Users can now pay for the 'Pro' plan. However, for a modern SAAS, especially an AI SAAS, simply distinguishing between 'free' and 'paid' is far from sufficient.