React SaaS Template
A complete, production-ready React SaaS starter that handles the infrastructure every SaaS needs but nobody wants to build twice. Ships with TypeScript, authentication with RBAC, Stripe billing, a responsive dashboard, and a component library on Tailwind CSS. Fork it, connect Stripe, and start building features on day one.
Key Features
- Authentication & RBAC — NextAuth.js with email/password, OAuth, magic links, and role-based middleware for admin/member/viewer permissions
- Stripe Billing Integration — Subscription management, pricing page, customer portal, webhook handlers, and usage-based metering support
- Dashboard Layout — Responsive sidebar navigation, breadcrumbs, command palette (Cmd+K), notification center, and user settings
- Data Tables — Sortable, filterable, paginated tables with TanStack Table, row selection, bulk actions, and CSV export
- Component Library — 40+ components: buttons, forms, modals, dropdowns, toasts, badges, avatars, and data visualization cards
- Multi-Tenant Ready — Organization model with invite flows and per-tenant data isolation
- Type-Safe API — tRPC or REST with Zod validation and auto-generated TypeScript types
Quick Start
# Clone and install
npx degit your-org/react-saas-template my-saas
cd my-saas
npm install
# Configure environment
cp .env.example .env.local
# Edit with your database, Stripe, and auth credentials
# Set up database and seed demo data
npx prisma migrate dev --name init
npx prisma db seed
# Start development
npm run dev
Open http://localhost:3000 — log in with admin@example.com / password123 to see the full dashboard.
Architecture / How It Works
react-saas-template/
├── app/(marketing)/ # Landing, pricing, blog (public)
├── app/(app)/ # Dashboard, settings (authenticated)
├── app/api/ # Auth, Stripe webhooks, API v1
├── components/ # ui/, dashboard/, billing/, data-table/
├── lib/ # auth.ts, stripe.ts, db.ts, permissions.ts
├── prisma/ # User, Team, Subscription models
└── emails/ # React Email templates (welcome, invite)
Usage Examples
Protected API Route with RBAC
// app/api/v1/team/members/route.ts
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { requireRole } from '@/lib/permissions';
import { db } from '@/lib/db';
export async function DELETE(request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
// Only admins can remove team members
requireRole(session.user, 'admin');
const { memberId } = await request.json();
await db.teamMember.delete({ where: { id: memberId, teamId: session.user.teamId } });
return NextResponse.json({ success: true });
}
Stripe Subscription Checkout
'use client';
import { useState } from 'react';
export function PricingCard({ plan }: { plan: Plan }) {
const [loading, setLoading] = useState(false);
async function handleSubscribe() {
setLoading(true);
const res = await fetch('/api/stripe/checkout', { method: 'POST', body: JSON.stringify({ priceId: plan.stripePriceId }) });
const { url } = await res.json();
window.location.href = url;
}
return (
<div className="rounded-xl border p-6">
<h3 className="text-xl font-bold">{plan.name}</h3>
<p className="text-3xl font-bold mt-2">${plan.price}<span className="text-sm">/mo</span></p>
<button onClick={handleSubscribe} disabled={loading} className="mt-6 w-full rounded-lg bg-blue-600 px-4 py-2 text-white">
{loading ? 'Redirecting...' : 'Subscribe'}
</button>
</div>
);
}
Stripe Webhook Handler
// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';
import { headers } from 'next/headers';
import { db } from '@/lib/db';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: NextRequest) {
const body = await request.text();
const sig = headers().get('stripe-signature')!;
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
if (event.type === 'customer.subscription.updated') {
const sub = event.data.object as Stripe.Subscription;
await db.subscription.update({
where: { stripeSubscriptionId: sub.id },
data: { status: sub.status, currentPeriodEnd: new Date(sub.current_period_end * 1000) },
});
}
return NextResponse.json({ received: true });
}
Configuration
Stripe Environment Variables
STRIPE_SECRET_KEY="sk_test_YOUR_KEY_HERE"
STRIPE_PUBLISHABLE_KEY="pk_test_YOUR_KEY_HERE"
STRIPE_WEBHOOK_SECRET="whsec_YOUR_WEBHOOK_SECRET"
# Product/Price IDs from your Stripe dashboard
STRIPE_STARTER_PRICE_ID="price_YOUR_STARTER_PRICE"
STRIPE_PRO_PRICE_ID="price_YOUR_PRO_PRICE"
Prisma Schema (Subscription Model)
model Subscription {
id String @id @default(cuid())
userId String @unique
stripeSubscriptionId String @unique
status String
currentPeriodEnd DateTime
user User @relation(fields: [userId], references: [id])
}
Best Practices
- Never trust the client for billing — always verify subscription status server-side via Stripe webhooks
- Use Stripe Customer Portal — let Stripe handle plan changes and payment methods
- Separate marketing and app layouts — use route groups to avoid loading dashboard JS on public pages
- Gate features in middleware — check permissions in Next.js middleware, not just UI
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Stripe webhook returns 400 | Raw body parsed before signature verification | Use request.text() not request.json() before constructEvent
|
| Session is null after login |
NEXTAUTH_SECRET not set or mismatched |
Generate with openssl rand -base64 32 and set in .env.local
|
| Billing page shows stale data | Subscription status not synced from webhook | Verify webhook endpoint is registered in Stripe Dashboard |
| Role check fails for admin | User role not included in session token | Add role to session callback in NextAuth config |
This is 1 of 11 resources in the Frontend Developer Pro toolkit. Get the complete [React SaaS Template] with all files, templates, and documentation for $59.
Or grab the entire Frontend Developer Pro bundle (11 products) for $129 — save 30%.
Top comments (0)