Learn Build a Real SaaS with Claude Code Scaffolding the Stack

Scaffolding the Stack

Intermediate 🕐 24 min Lesson 3 of 25
What you'll learn
  • Scaffold a Next.js + TypeScript + Tailwind project using the framework's own generator instead of hand-writing config
  • Identify Tailwind v4's CSS-first configuration (@theme inline) as a convention shift from earlier versions
  • Install and configure Drizzle ORM and drizzle-kit with a drizzle.config.ts pointing at a schema file and output directory
  • Explain the purpose-driven directory split between db/ and lib/ and why it matters for later data access patterns
  • Set up the db:generate, db:migrate, db:studio, db:seed, and test npm scripts used throughout the rest of the build

Start With the Framework's Own Generator

The reference build's very first commit, ce4bfad Initial commit from Create Next App, is the plain output of Next.js's own scaffolding tool. This matters more than it sounds: do not ask Claude Code to hand-write a Next.js project structure from scratch. Let the framework's generator produce the boilerplate -- correct config files, correct folder conventions, correct dependency versions -- then have Claude Code build on top of a known-good foundation instead of reinventing one.

npx create-next-app@latest reputation-dashboard --typescript --tailwind --app

That single command is responsible for most of what you see in the reference repo's root: next.config.ts, tsconfig.json, eslint.config.mjs, the app/ directory with App Router conventions, and Tailwind already wired through PostCSS. Check postcss.config.mjs and you will find Tailwind v4's plugin-based setup -- @tailwindcss/postcss -- and app/globals.css pulls it in with a single line:

@import "tailwindcss";

:root {
  --background: #ffffff;
  --foreground: #171717;
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

Tailwind v4 configures itself in CSS, not a tailwind.config.js file -- the @theme inline block above is where design tokens like colors and fonts live. If you have used an earlier Tailwind version, this is the single biggest convention shift to be aware of, and it is exactly the kind of thing the project's AGENTS.md from lesson 2 warns about: do not assume your training data's Tailwind config syntax is current.

Adding Supabase and Drizzle

With the Next.js scaffold committed, the next step adds the two libraries that give the app a real database and real auth:

npm install @supabase/ssr @supabase/supabase-js drizzle-orm postgres
npm install -D drizzle-kit tsx dotenv

drizzle-orm and postgres (the postgres-js driver) are runtime dependencies; drizzle-kit generates and runs migrations and is dev-only. tsx runs TypeScript files directly, which the reference build uses for its migration and seed scripts. Drizzle needs a config file telling it where your schema lives and where to write generated migrations -- this is drizzle.config.ts in the repo root:

import { defineConfig } from "drizzle-kit";
import "dotenv/config";

if (!process.env.DATABASE_URL) {
  throw new Error("DATABASE_URL is not set");
}

export default defineConfig({
  schema: "./db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL,
  },
  verbose: true,
  strict: true,
});

Project Structure That Emerges

By the end of scaffolding, the directory layout is already predicting the rest of the build:

Path Purpose
app/
Next.js App Router pages, layouts, and API route handlers
db/
Drizzle schema, queries, migration runner, seed script
drizzle/
Generated SQL migration files (drizzle-kit output, not hand-written)
lib/
Supabase clients, auth helpers, billing/email logic
components/
Shared UI pieces (badges, rating stars)

This is not an accident of taste -- it is App Router's own convention plus a deliberate separation of "what touches the database" (db/) from "what touches third-party services" (lib/). Ask Claude Code to follow this split explicitly rather than letting database queries leak into route handlers or components, because that separation is what makes lesson 7's data access patterns possible.

package.json Scripts Worth Setting Up Now

The reference build wires five scripts that the rest of this track uses constantly:

"db:generate": "drizzle-kit generate",
"db:migrate": "tsx db/migrate.ts",
"db:studio": "drizzle-kit studio",
"db:seed": "tsx db/seed.ts",
"test": "vitest run"

db:generate diffs your schema file against the last migration and writes a new SQL migration -- it does not touch your actual database. db:migrate applies pending migrations. Keeping these as separate, explicit steps (rather than one command that does both) is deliberate: you want to review a generated migration before running it against real data, especially once there is real data to lose.

Set up the five npm scripts for Drizzle generate, migrate, studio, and seed, plus the Vitest test script, matching the reputation dashboard reference build's package.json.

What Comes Next

At the end of this lesson you have a committed Next.js + Tailwind scaffold, Drizzle and Supabase installed and configured to point at a database, and a project structure that already separates concerns correctly. You do not have a schema yet, or auth, or a single feature -- and that is the right place to stop. Lesson 4 covers the git and PR workflow this build actually used from here forward: small, reviewed, incremental commits, not one giant generation.

Key takeaways
  • Letting create-next-app generate the initial scaffold avoids reinventing known-good config and folder conventions by hand
  • Tailwind v4 configures design tokens in CSS via @theme inline, not in a tailwind.config.js file -- the single biggest convention change from earlier Tailwind versions
  • drizzle-kit (dev dependency, generates/runs migrations) is separate from drizzle-orm (runtime dependency, the query builder) -- both are needed but serve different roles
  • db:generate only diffs the schema and writes a migration file; it never touches the live database -- db:migrate is the separate, explicit step that applies it
  • Splitting db/ (database access) from lib/ (third-party service clients and business logic) early prevents queries from leaking into routes and components later