Next.js + Supabase SaaS Planner
Overview
Expert SaaS planning assistant that transforms product ideas into actionable technical roadmaps. Specializes in the Next.js 15 + Supabase stack, covering architecture design, database schemas, authentication patterns, multi-tenancy, billing integration, and launch strategies.
When to use this skill:
- •Planning a new SaaS product from scratch
- •Converting a product idea into technical specifications
- •Designing database schemas for SaaS applications
- •Setting up authentication and authorization
- •Implementing multi-tenant architecture
- •Integrating subscription billing (Stripe, Paddle)
- •Planning feature rollout and MVP priorities
- •Creating development roadmaps and timelines
Planning Workflow
Phase 1: Discovery and Requirements
Ask these questions first:
- •
Product Overview
- •What problem does your SaaS solve?
- •Who is your target user?
- •What's your unique value proposition?
- •
Scale Expectations
- •How many users in first year? (0-1K, 1K-10K, 10K-100K, 100K+)
- •Expected growth rate?
- •Geographic distribution?
- •
Team and Timeline
- •Team size and skills?
- •Launch timeline? (MVP date)
- •Budget constraints?
- •
Business Model
- •Pricing tiers?
- •Free trial or freemium?
- •What features differentiate paid tiers?
- •
Core Features
- •Must-have features for MVP?
- •Nice-to-have features for v1.1?
- •Future roadmap ideas?
Phase 2: Technical Architecture Planning
Based on requirements, create:
- •System Architecture Diagram
- •Database Schema
- •File/Folder Structure
- •Authentication Flow
- •Multi-Tenancy Strategy (if applicable)
- •Billing Integration Plan
- •Deployment Architecture
Phase 3: Development Roadmap
Create week-by-week plan:
- •Week 1-2: Setup and infrastructure
- •Week 3-4: Core features
- •Week 5-6: Authentication and billing
- •Week 7-8: Polish and testing
- •Week 9: Launch preparation
Next.js 15 + Supabase Stack (2025)
Recommended Tech Stack
Frontend: ├─ Next.js 15 (App Router) ├─ React 19 ├─ TypeScript ├─ Tailwind CSS └─ shadcn/ui components Backend: ├─ Next.js API Routes (Server Actions) ├─ Supabase Edge Functions (when needed) └─ Stripe/Paddle webhooks Database: ├─ PostgreSQL (Supabase) ├─ Row Level Security (RLS) └─ Supabase Realtime (optional) Authentication: ├─ Supabase Auth ├─ OAuth providers (Google, GitHub, etc.) └─ Magic links Storage: └─ Supabase Storage (files, images) Deployment: ├─ Vercel (frontend + API) ├─ Supabase (database, auth, storage) └─ Cloudflare (CDN, DNS) Payments: ├─ Stripe (recommended) └─ Paddle (alternative) Monitoring: ├─ Vercel Analytics ├─ Sentry (error tracking) └─ PostHog (product analytics - optional)
Why This Stack?
Next.js 15:
- •Server Components for performance
- •Server Actions for mutations
- •Built-in caching strategies
- •Excellent DX and fast iteration
Supabase:
- •Instant REST API
- •Real-time subscriptions
- •Built-in authentication
- •Row-level security
- •Open source (no vendor lock-in risk)
TypeScript:
- •Type safety reduces bugs
- •Better IDE support
- •Self-documenting code
Stripe:
- •Industry standard for payments
- •Excellent documentation
- •Subscription management
- •Invoice handling
File Structure Template
my-saas/ ├─ app/ │ ├─ (auth)/ │ │ ├─ login/ │ │ │ └─ page.tsx │ │ ├─ signup/ │ │ │ └─ page.tsx │ │ ├─ forgot-password/ │ │ │ └─ page.tsx │ │ └─ layout.tsx │ │ │ ├─ (marketing)/ │ │ ├─ page.tsx # Landing page │ │ ├─ pricing/ │ │ │ └─ page.tsx │ │ ├─ about/ │ │ │ └─ page.tsx │ │ └─ layout.tsx │ │ │ ├─ (dashboard)/ │ │ ├─ dashboard/ │ │ │ └─ page.tsx │ │ ├─ settings/ │ │ │ ├─ profile/ │ │ │ │ └─ page.tsx │ │ │ ├─ billing/ │ │ │ │ └─ page.tsx │ │ │ └─ team/ │ │ │ └─ page.tsx │ │ └─ layout.tsx │ │ │ ├─ api/ │ │ ├─ webhooks/ │ │ │ └─ stripe/ │ │ │ └─ route.ts │ │ └─ og/ # Open Graph images │ │ └─ route.tsx │ │ │ ├─ layout.tsx # Root layout │ └─ globals.css │ ├─ components/ │ ├─ ui/ # shadcn components │ │ ├─ button.tsx │ │ ├─ card.tsx │ │ ├─ dialog.tsx │ │ └─ ... │ ├─ auth/ │ │ ├─ login-form.tsx │ │ └─ signup-form.tsx │ ├─ dashboard/ │ │ ├─ sidebar.tsx │ │ └─ navbar.tsx │ ├─ marketing/ │ │ ├─ hero.tsx │ │ ├─ features.tsx │ │ └─ pricing-cards.tsx │ └─ shared/ │ ├─ user-avatar.tsx │ └─ loading-spinner.tsx │ ├─ lib/ │ ├─ supabase/ │ │ ├─ client.ts # Client-side Supabase │ │ ├─ server.ts # Server-side Supabase │ │ └─ middleware.ts # Auth middleware │ ├─ stripe/ │ │ ├─ client.ts │ │ └─ server.ts │ ├─ db/ │ │ └─ queries.ts # Database queries │ ├─ auth.ts # Auth helpers │ └─ utils.ts # General utilities │ ├─ hooks/ │ ├─ use-user.ts # User session hook │ ├─ use-subscription.ts # Subscription status │ └─ use-toast.ts │ ├─ actions/ # Server Actions │ ├─ auth/ │ │ ├─ login.ts │ │ ├─ signup.ts │ │ └─ logout.ts │ ├─ billing/ │ │ ├─ create-checkout.ts │ │ └─ cancel-subscription.ts │ └─ profile/ │ └─ update-profile.ts │ ├─ types/ │ ├─ database.ts # Generated from Supabase │ ├─ supabase.ts │ └─ stripe.ts │ ├─ supabase/ │ ├─ migrations/ # Database migrations │ │ └─ 20250101000000_initial.sql │ ├─ functions/ # Edge Functions (optional) │ └─ config.toml │ ├─ public/ │ ├─ images/ │ └─ icons/ │ ├─ middleware.ts # Next.js middleware (auth) ├─ next.config.js ├─ tailwind.config.js ├─ tsconfig.json ├─ package.json └─ .env.local
Database Schema Patterns
Core Tables for SaaS
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Users table (extended from Supabase auth.users)
CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
email TEXT UNIQUE NOT NULL,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Organizations/Teams (multi-tenant)
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
logo_url TEXT,
subscription_tier TEXT DEFAULT 'free',
subscription_status TEXT DEFAULT 'active',
stripe_customer_id TEXT UNIQUE,
stripe_subscription_id TEXT UNIQUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Organization memberships
CREATE TABLE organization_members (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member', -- 'owner', 'admin', 'member'
invited_by UUID REFERENCES profiles(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(organization_id, user_id)
);
-- Subscription plans
CREATE TABLE plans (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
description TEXT,
price_monthly DECIMAL(10,2),
price_yearly DECIMAL(10,2),
stripe_price_id_monthly TEXT,
stripe_price_id_yearly TEXT,
features JSONB,
limits JSONB, -- { "users": 5, "projects": 10 }
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Subscription usage tracking
CREATE TABLE usage_records (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
metric TEXT NOT NULL, -- 'api_calls', 'storage_mb', 'users'
value INTEGER NOT NULL,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Invitations
CREATE TABLE invitations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
email TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member',
token TEXT UNIQUE NOT NULL,
invited_by UUID REFERENCES profiles(id),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
accepted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Audit log (optional but recommended)
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id),
user_id UUID REFERENCES profiles(id),
action TEXT NOT NULL,
resource_type TEXT,
resource_id UUID,
metadata JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_org_members_org ON organization_members(organization_id);
CREATE INDEX idx_org_members_user ON organization_members(user_id);
CREATE INDEX idx_usage_org_period ON usage_records(organization_id, period_start, period_end);
CREATE INDEX idx_invitations_token ON invitations(token);
CREATE INDEX idx_audit_logs_org ON audit_logs(organization_id);
Row Level Security (RLS) Policies
-- Enable RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
-- Profiles: Users can read and update their own profile
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- Organizations: Members can read their organizations
CREATE POLICY "Members can view their organizations"
ON organizations FOR SELECT
USING (
id IN (
SELECT organization_id
FROM organization_members
WHERE user_id = auth.uid()
)
);
-- Only owners can update organizations
CREATE POLICY "Owners can update organizations"
ON organizations FOR UPDATE
USING (
id IN (
SELECT organization_id
FROM organization_members
WHERE user_id = auth.uid() AND role = 'owner'
)
);
-- Organization members: Members can view other members
CREATE POLICY "Members can view team members"
ON organization_members FOR SELECT
USING (
organization_id IN (
SELECT organization_id
FROM organization_members
WHERE user_id = auth.uid()
)
);
Authentication Setup
Supabase Auth Configuration
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options });
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options });
},
},
}
);
}
Auth Middleware
// middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
response.cookies.set({
name,
value,
...options,
});
},
remove(name: string, options: CookieOptions) {
response.cookies.set({
name,
value: '',
...options,
});
},
},
}
);
const { data: { user } } = await supabase.auth.getUser();
// Redirect to login if not authenticated
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Redirect to dashboard if already authenticated
if (user && (
request.nextUrl.pathname === '/login' ||
request.nextUrl.pathname === '/signup'
)) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return response;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
Stripe Integration
Setup
// lib/stripe/server.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
});
// Create checkout session
export async function createCheckoutSession(
organizationId: string,
priceId: string,
userId: string
) {
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/billing?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
client_reference_id: organizationId,
metadata: {
organizationId,
userId,
},
});
return session;
}
Webhook Handler
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { stripe } from '@/lib/stripe/server';
import { createClient } from '@/lib/supabase/server';
export async function POST(req: Request) {
const body = await req.text();
const signature = (await headers()).get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
const supabase = await createClient();
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object as Stripe.Checkout.Session;
await supabase
.from('organizations')
.update({
stripe_customer_id: session.customer as string,
stripe_subscription_id: session.subscription as string,
subscription_status: 'active',
})
.eq('id', session.metadata?.organizationId);
break;
case 'customer.subscription.updated':
const subscription = event.data.object as Stripe.Subscription;
await supabase
.from('organizations')
.update({
subscription_status: subscription.status,
})
.eq('stripe_subscription_id', subscription.id);
break;
case 'customer.subscription.deleted':
const deletedSub = event.data.object as Stripe.Subscription;
await supabase
.from('organizations')
.update({
subscription_status: 'canceled',
subscription_tier: 'free',
})
.eq('stripe_subscription_id', deletedSub.id);
break;
}
return NextResponse.json({ received: true });
}
Multi-Tenancy Patterns
Organization-Based (Recommended)
Pattern: Each organization has its own data space.
Benefits:
- •Clear data isolation
- •Easy to implement
- •Scalable per organization
- •Simple billing per organization
Implementation:
// All queries include organization_id
const { data } = await supabase
.from('projects')
.select('*')
.eq('organization_id', currentOrgId);
Subdomain-Based (Advanced)
Pattern: Each organization has its own subdomain (e.g., acme.yourapp.com).
Benefits:
- •Better branding
- •Clear isolation
- •Professional appearance
Challenges:
- •DNS management
- •SSL certificates
- •More complex routing
Implementation:
// middleware.ts
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host')!;
const subdomain = hostname.split('.')[0];
// Fetch organization by subdomain
// Set in request context
}
MVP Feature Prioritization
Week 1-2: Foundation
- • Project setup (Next.js + Supabase)
- • Database schema creation
- • Authentication (email + OAuth)
- • Basic layouts and routing
- • Landing page
Week 3-4: Core Features
- • Dashboard layout
- • First core feature (main value prop)
- • User profile management
- • Organization/team creation
- • Team member invitations
Week 5-6: Monetization
- • Pricing page
- • Stripe integration
- • Subscription management
- • Usage limits enforcement
- • Billing page
Week 7-8: Polish
- • Email notifications
- • Error handling
- • Loading states
- • Responsive design
- • SEO optimization
Week 9: Launch Prep
- • Testing (E2E, unit)
- • Security audit
- • Performance optimization
- • Documentation
- • Deployment scripts
Environment Variables
# .env.local # Supabase NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key SUPABASE_SERVICE_ROLE_KEY=your-service-key # Stripe STRIPE_SECRET_KEY=sk_test_xxxxx NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx STRIPE_WEBHOOK_SECRET=whsec_xxxxx # App NEXT_PUBLIC_APP_URL=http://localhost:3000 # Optional SENTRY_DSN=your-sentry-dsn POSTHOG_KEY=your-posthog-key
Deployment Checklist
Pre-Launch
- • Environment variables configured
- • Database migrations run
- • RLS policies tested
- • Stripe webhooks configured
- • Domain configured
- • SSL certificates active
- • Analytics setup
- • Error tracking (Sentry)
- • Email service configured
- • Terms of Service + Privacy Policy pages
Post-Launch
- • Monitor error rates
- • Check subscription flows
- • Test webhook delivery
- • Monitor database performance
- • Set up backups
- • Create status page
- • Implement feature flags
Best Practices
Performance
- •Use Server Components by default
- •Implement proper caching strategies
- •Optimize images with
next/image - •Use React Server Actions for mutations
- •Lazy load heavy components
Security
- •Always use RLS policies
- •Validate inputs on server
- •Never expose service role key to client
- •Use prepared statements (Supabase handles this)
- •Implement rate limiting for sensitive endpoints
Development
- •Use TypeScript strictly
- •Write tests for critical paths
- •Use database migrations (never manual changes)
- •Version your API
- •Document complex logic
User Experience
- •Add loading states everywhere
- •Implement optimistic updates
- •Show clear error messages
- •Make onboarding smooth
- •Add helpful empty states
Common Patterns Reference
For detailed implementation patterns, see reference documents:
- •
AUTHENTICATION_PATTERNS.md- OAuth, magic links, MFA - •
BILLING_PATTERNS.md- Stripe integration, webhooks, metering - •
MULTI_TENANT_PATTERNS.md- Organization structures, data isolation - •
DEPLOYMENT_STRATEGIES.md- CI/CD, environments, monitoring
Example Output Format
When planning a SaaS, provide:
- •
Executive Summary
- •Product overview
- •Target users
- •Key features
- •
Technical Architecture
- •System diagram
- •Tech stack rationale
- •Database schema
- •
File Structure
- •Complete folder organization
- •Key file descriptions
- •
Development Roadmap
- •Week-by-week breakdown
- •Feature prioritization
- •Estimated effort
- •
Deployment Plan
- •Infrastructure setup
- •Environment configuration
- •Launch checklist
- •
Next Steps
- •Immediate actions
- •Setup commands
- •Documentation links
Success Criteria
A well-planned SaaS should have:
- •✅ Clear database schema with RLS
- •✅ Secure authentication flow
- •✅ Scalable architecture
- •✅ Working billing integration
- •✅ Organized file structure
- •✅ Realistic timeline
- •✅ Clear MVP scope
- •✅ Deployment strategy