Build a SaaS Starter Kit
SaaS Project Setup & Architecture
5 min read
What You'll Build
- Next.js 15 with App Router and TypeScript
- Tailwind CSS with a professional design system
- PostgreSQL database with Drizzle ORM
- Project structure optimized for AI-assisted development
- Environment configuration for development and production
Time to complete: ~30 minutes with AI assistance (vs 2-3 hours manually)
Step 1: Initialize with AI
Open your terminal and start Claude Code (or Cursor with Agent mode). Use this prompt:
Create a new Next.js 15 SaaS starter with:
- TypeScript in strict mode
- Tailwind CSS with a custom color palette (primary: indigo, secondary: slate)
- App Router with (marketing), (dashboard), and (auth) route groups
- PostgreSQL with Drizzle ORM
- shadcn/ui components (button, card, input, dialog)
- Environment variables setup (.env.example)
Project name: my-saas-starter
Use pnpm as package manager
What AI does:
- Runs
pnpm create next-appwith correct flags - Installs and configures Tailwind with custom theme
- Sets up Drizzle ORM with PostgreSQL adapter
- Creates route group structure
- Initializes shadcn/ui
- Creates .env.example with required variables
Step 2: Project Structure
After AI completes setup, you should have:
my-saas-starter/
├── src/
│ ├── app/
│ │ ├── (marketing)/ # Landing, pricing, about
│ │ │ ├── page.tsx # Home page
│ │ │ ├── pricing/
│ │ │ └── layout.tsx
│ │ ├── (dashboard)/ # Protected app pages
│ │ │ ├── dashboard/
│ │ │ ├── settings/
│ │ │ └── layout.tsx
│ │ ├── (auth)/ # Login, signup, forgot password
│ │ │ ├── login/
│ │ │ ├── signup/
│ │ │ └── layout.tsx
│ │ ├── api/ # API routes
│ │ ├── layout.tsx # Root layout
│ │ └── globals.css
│ ├── components/
│ │ ├── ui/ # shadcn components
│ │ ├── marketing/ # Landing page components
│ │ └── dashboard/ # Dashboard components
│ ├── lib/
│ │ ├── db/ # Database client & schema
│ │ ├── auth/ # Auth utilities
│ │ └── utils.ts
│ └── types/
├── drizzle/ # Migrations
├── public/
├── .env.example
├── drizzle.config.ts
├── tailwind.config.ts
└── package.json
Step 3: Database Schema Design
Ask AI to create your initial database schema:
Create a Drizzle schema for a multi-tenant SaaS with:
- users table (id, email, name, avatar, created_at, updated_at)
- organizations table (id, name, slug, owner_id, plan, created_at)
- organization_members table (user_id, org_id, role: owner/admin/member)
- subscriptions table (id, org_id, stripe_customer_id, stripe_subscription_id, status, plan, current_period_end)
Use UUID for IDs, proper indexes, and foreign key relationships.
File: src/lib/db/schema.ts
AI generates:
// src/lib/db/schema.ts
import { pgTable, uuid, text, timestamp, pgEnum, index } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Enums
export const memberRoleEnum = pgEnum('member_role', ['owner', 'admin', 'member']);
export const subscriptionStatusEnum = pgEnum('subscription_status', [
'active', 'canceled', 'past_due', 'trialing', 'incomplete'
]);
export const planEnum = pgEnum('plan', ['free', 'starter', 'pro', 'enterprise']);
// Users
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name'),
avatarUrl: text('avatar_url'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => ({
emailIdx: index('users_email_idx').on(table.email),
}));
// Organizations
export const organizations = pgTable('organizations', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
ownerId: uuid('owner_id').references(() => users.id).notNull(),
plan: planEnum('plan').default('free').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => ({
slugIdx: index('orgs_slug_idx').on(table.slug),
ownerIdx: index('orgs_owner_idx').on(table.ownerId),
}));
// Organization Members
export const organizationMembers = pgTable('organization_members', {
userId: uuid('user_id').references(() => users.id).notNull(),
organizationId: uuid('organization_id').references(() => organizations.id).notNull(),
role: memberRoleEnum('role').default('member').notNull(),
joinedAt: timestamp('joined_at').defaultNow().notNull(),
}, (table) => ({
pk: index('org_members_pk').on(table.userId, table.organizationId),
}));
// Subscriptions
export const subscriptions = pgTable('subscriptions', {
id: uuid('id').primaryKey().defaultRandom(),
organizationId: uuid('organization_id').references(() => organizations.id).notNull(),
stripeCustomerId: text('stripe_customer_id'),
stripeSubscriptionId: text('stripe_subscription_id'),
status: subscriptionStatusEnum('status').default('trialing').notNull(),
plan: planEnum('plan').default('free').notNull(),
currentPeriodEnd: timestamp('current_period_end'),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => ({
orgIdx: index('subscriptions_org_idx').on(table.organizationId),
stripeSubIdx: index('subscriptions_stripe_idx').on(table.stripeSubscriptionId),
}));
// Relations
export const usersRelations = relations(users, ({ many }) => ({
ownedOrganizations: many(organizations),
memberships: many(organizationMembers),
}));
export const organizationsRelations = relations(organizations, ({ one, many }) => ({
owner: one(users, {
fields: [organizations.ownerId],
references: [users.id],
}),
members: many(organizationMembers),
subscription: one(subscriptions),
}));
Step 4: Environment Configuration
Create proper environment handling:
Create environment configuration with:
1. .env.example with all required variables (documented)
2. src/lib/env.ts with Zod validation for type-safe env access
3. Include: DATABASE_URL, NEXTAUTH_*, STRIPE_*, APP_URL
AI generates:
// src/lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
// Database
DATABASE_URL: z.string().url(),
// Auth
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
// OAuth Providers
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
GITHUB_CLIENT_ID: z.string().optional(),
GITHUB_CLIENT_SECRET: z.string().optional(),
// Stripe
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
STRIPE_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
// App
NEXT_PUBLIC_APP_URL: z.string().url(),
// Optional
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
// Parse and validate
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('❌ Invalid environment variables:');
console.error(parsed.error.flatten().fieldErrors);
throw new Error('Invalid environment variables');
}
export const env = parsed.data;
Step 5: Design System Setup
Ask AI to create a consistent design system:
Create a Tailwind design system config with:
- Primary color: Indigo (for CTAs, links)
- Secondary color: Slate (for text, backgrounds)
- Success/Warning/Error colors
- Typography scale (display, heading, body, small)
- Spacing consistent with 4px grid
- Border radius tokens (sm, md, lg, xl, full)
- Shadow tokens (sm, md, lg, glow)
- Animation durations
Update tailwind.config.ts with these tokens.
Step 6: Create Base Components
Use AI to generate foundational components:
Create these base components in src/components/:
1. ui/container.tsx - Max-width centered container with responsive padding
2. ui/section.tsx - Page section with vertical spacing
3. ui/heading.tsx - Typography component with size variants
4. marketing/navbar.tsx - Marketing site header with mobile menu
5. marketing/footer.tsx - Site footer with links
6. dashboard/sidebar.tsx - Dashboard navigation sidebar
7. dashboard/header.tsx - Dashboard top bar with user menu
Use shadcn/ui components where appropriate.
Follow the design system tokens.
Include TypeScript types for all props.
Step 7: Database Setup
Run the initial migration:
# Create the database (using Docker for local dev)
docker run --name saas-postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=saas -p 5432:5432 -d postgres:16
# Generate migration
pnpm drizzle-kit generate
# Push to database
pnpm drizzle-kit push
Ask AI to create a database utility:
Create src/lib/db/index.ts with:
- Database client singleton
- Helper function for transactions
- Connection pooling configuration for serverless (Neon/Vercel Postgres)
Step 8: Verify Setup
Ask AI to create a simple test page:
Create a test page at src/app/(marketing)/page.tsx that:
- Uses the Container and Section components
- Shows the Navbar and Footer
- Displays a hero section with headline, subheadline, and CTA button
- Shows 3 feature cards using shadcn Card component
- Is fully responsive
Hero text:
- Headline: "Build faster with AI-powered development"
- Subheadline: "The modern SaaS starter kit for vibe coders"
- CTA: "Get Started Free"
Run the dev server:
pnpm dev
Visit http://localhost:3000 to see your foundation.
Key Takeaways
- AI accelerates scaffolding - Setup that takes hours manually takes minutes with AI
- Structure matters - Route groups and organized folders make AI context more effective
- Type safety pays off - TypeScript and Zod validation catch errors early
- Design systems scale - Tokens ensure consistency as your app grows
- Database design first - Well-designed schemas save refactoring later
What's Next
In the next lesson, you'll add authentication and user management to your SaaS foundation using NextAuth.js with multiple providers.
:::