DEV Community

Thesius Code
Thesius Code

Posted on • Originally published at datanest-stores.pages.dev

React SaaS Template

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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.

Get the Full Kit →

Or grab the entire Frontend Developer Pro bundle (11 products) for $129 — save 30%.

Get the Complete Bundle →


Related Articles

Top comments (0)