Production-ready SaaS boilerplate with Next.js, FastAPI, Supabase, and Stripe.
| Layer | Technology |
|---|---|
| Frontend | Next.js 16 + Tailwind 4 + Catalyst UI + Headless UI |
| Backend | FastAPI + Pydantic + httpx |
| Auth | Supabase (Google OAuth) |
| Database | Supabase Postgres (REST API) |
| Payments | Stripe (Checkout + Subscriptions + Webhooks) |
| Encryption | Fernet (cryptography lib) |
- Go to supabase.com and create a new project
- Go to Authentication > Providers and enable Google (add your Google OAuth credentials)
- Go to SQL Editor and run
supabase/migrations/001_initial_schema.sql - Copy your project URL, anon key, and service role key
# Backend
cp backend/.env.example backend/.env
# Fill in: SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY
# Frontend
cp frontend/.env.example frontend/.env.local
# Fill in: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY# Backend (requires Python 3.12+ and uv)
cd backend
uv sync
# Frontend (requires Node.js 20+ and pnpm)
cd frontend
pnpm install# Terminal 1 — Backend
cd backend
MOCK_STRIPE=true uv run uvicorn app.main:app --reload --port 8000
# Terminal 2 — Frontend
cd frontend
pnpm dev- Open http://localhost:3000 — you should see the landing page
- Click "Get Started" — redirects to login
- Sign in with Google — redirects to dashboard
- Dashboard shows "All Systems Ready" with green badges
- Click "Call /api/v1/hello" — shows API response
/
├── frontend/ # Next.js app
│ ├── src/
│ │ ├── app/
│ │ │ ├── (marketing)/ # Landing page (/)
│ │ │ ├── login/ # Google OAuth login
│ │ │ ├── dashboard/ # Authenticated dashboard
│ │ │ └── auth/callback/ # OAuth callback handler
│ │ ├── components/ # Catalyst UI components
│ │ │ ├── button.tsx # Button with color variants
│ │ │ ├── badge.tsx # Status badges
│ │ │ ├── sidebar.tsx # Navigation sidebar
│ │ │ ├── sidebar-layout.tsx # Responsive sidebar layout
│ │ │ ├── dialog.tsx # Modal dialogs
│ │ │ ├── dropdown.tsx # Dropdown menus
│ │ │ ├── input.tsx # Form inputs
│ │ │ ├── heading.tsx # Typography
│ │ │ ├── text.tsx # Text components
│ │ │ ├── avatar.tsx # User avatars
│ │ │ ├── divider.tsx # Dividers
│ │ │ ├── navbar.tsx # Top navigation
│ │ │ ├── logo.tsx # App logo
│ │ │ └── link.tsx # Next.js Link wrapper
│ │ └── lib/
│ │ ├── api.ts # API client (typed, auth headers)
│ │ └── supabase/ # Supabase client/server/middleware
│ └── .env.example
│
├── backend/ # FastAPI app
│ ├── app/
│ │ ├── main.py # App setup, CORS, health, routers
│ │ ├── config.py # Pydantic Settings from env
│ │ ├── auth.py # Supabase JWT validation
│ │ ├── database.py # Supabase REST API client
│ │ ├── schemas.py # Pydantic request/response models
│ │ ├── routers/
│ │ │ ├── hello.py # GET /hello (test endpoint)
│ │ │ ├── users.py # GET /users/me
│ │ │ ├── checkout.py # POST /checkout, GET /subscription
│ │ │ └── webhooks.py # POST /webhooks/stripe
│ │ └── services/
│ │ └── encryption.py # Fernet encrypt/decrypt
│ └── .env.example
│
├── supabase/
│ └── migrations/
│ └── 001_initial_schema.sql # Subscriptions table + RLS
│
├── CLAUDE.md # AI agent instructions
└── README.md # This file
Browser → Next.js (Vercel)
↓ API calls with Supabase JWT
FastAPI (Render/Railway)
↓ REST API with service role key
Supabase Postgres
- Frontend never talks to Supabase directly for data — all through FastAPI
- Frontend uses Supabase only for auth (login, session, JWT)
- Backend validates JWT on every request via Supabase
/auth/v1/user - Backend uses service role key for all database operations
- User clicks "Sign in with Google" on
/login - Supabase redirects to Google consent screen
- Google redirects back to
/auth/callback - Callback exchanges code for session (sets cookies)
- Middleware redirects to
/dashboard - Dashboard calls backend API with JWT in
Authorizationheader - Backend validates JWT against Supabase
- User clicks "Subscribe" →
POST /api/v1/checkout - Backend creates Stripe Checkout Session → returns URL
- Frontend redirects to Stripe Checkout
- User pays → Stripe webhook fires →
POST /api/v1/webhooks/stripe - Backend creates/updates subscription record in DB
- Dashboard shows subscription status
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Forward webhooks
stripe listen --forward-to localhost:8000/api/v1/webhooks/stripe
# Copy the webhook signing secret (whsec_...) to your .env- Create
backend/app/routers/my_feature.py - Add Pydantic models to
backend/app/schemas.py - Register in
backend/app/main.py:app.include_router(my_feature.router, prefix="/api/v1") - Add API function in
frontend/src/lib/api.ts
- Create
supabase/migrations/002_my_table.sql - Run it in Supabase SQL Editor
- Use
db.select(),db.insert(), etc. in your router
- Add a new section type to the
Sectionunion indashboard/page.tsx - Add a
SidebarItemin the sidebar - Add content in the section rendering block
| Service | Deploy To | Notes |
|---|---|---|
| Frontend | Vercel | cd frontend && pnpm build |
| Backend | Render / Railway | cd backend && uv run uvicorn app.main:app |
| Database | Supabase | Cloud-hosted Postgres |
Update APP_URL and API_URL in both frontend and backend .env files to point to your production URLs. CORS is auto-configured from APP_URL.
For local development without Stripe credentials:
MOCK_STRIPE=true # Skips Stripe, auto-returns fake checkout URL
DEV_USER_ID=xxx # Bypass auth, use this Supabase user UUID