
Ship a Production-Ready Next.js SaaS Starter (With Auth, Billing, Analytics, and Admin) in a Weekend
This guide walks experienced JS/TS devs through building a production-ready Next.js SaaS starter in a weekend, including auth, billing, analytics, and admin. Learn the architecture, workflow, and pitfalls—and see how using a starter like `starter` can cut that time down to hours.
Ship a Production-Ready Next.js SaaS Starter (With Auth, Billing, Analytics, and Admin) in a Weekend
Rebuilding login, subscriptions, analytics, and admin dashboards for every new SaaS or MVP is a tax on your focus. You know the core product is where your time should go, but you still end up wiring OAuth callbacks, Stripe webhooks, and user tables over and over.
If you search for a nextjs saas starter template with auth and billing, you’ll find a lot of boilerplates that solve part of the problem. This guide goes further: it shows you what “production-ready” actually means and how to get there in a single weekend.
Turn this idea into something you can actually ship.
If you want a cleaner workflow, better leverage, and a reusable foundation for your next product, start from the homepage and explore what this project is built to help you do.
By following this workflow, you’ll ship a usable baseline that you can reuse across projects, or shortcut the whole thing by adopting an existing starter like starter (Next.js + Clerk + billing + analytics + admin) and wiring it into your own “hub” for cross-project visibility.
What a Production-Ready Next.js SaaS Starter Needs

Before writing code, define what “done” means. A production-ready Next.js SaaS starter isn’t just “auth + Stripe”. It should include:
Authentication and User Management
- Email/password signup and login (ideally passwordless options too).
- Social providers (GitHub, Google) for faster onboarding.
- Session management and refresh (client + server).
- Basic roles/permissions: at minimum,
uservsadmin; ideally feature flags. - Secure route protection and server-side user context.
Clerk, Auth0, Supabase Auth, or a custom provider can all work. The key is that your auth layer:
- Exposes a clear server-side API (e.g.,
getAuth()orgetCurrentUser()). - Has a consistent user id you can use across billing, analytics, and the database.
Billing and Subscription Management
For a SaaS starter, billing should cover:
- Plans and pricing (monthly/annual, free/paid, trials).
- Checkout flows and a self-service customer portal.
- Handling invoices, payment failures, and cancellations.
- Webhooks to keep your local subscription state in sync with the billing provider.
- A clean
Subscriptiondomain model (even if the data is minimal at first).
Stripe is usually the default for B2B/B2C SaaS:
- Use Stripe Checkout or Billing Portal to avoid custom PCI scope early.
- Store Stripe
customer_idandsubscription_idagainst your app’s user. - Mirror minimal state locally: status, plan, renewal date, cancel_at.
Analytics and Product Metrics
You don’t need a full data warehouse to be production-ready. You do need:
- Basic traffic metrics (page views, referrers).
- Key events: signup, trial started, subscription activated, cancellation.
- Ability to track activation steps specific to your product.
- A low-friction way to view the data (third-party tool or a simple internal dashboard).
You can use:
- Product analytics (PostHog, Amplitude, Pirsch, etc.).
- A simple events table written from your app, plus a basic admin view.
- A central “hub” app that aggregates events from multiple starters/projects.
Admin and Internal Tools
You will need to inspect and fix things. Production-ready means:
- Admin auth: restrict access to internal tools via role or SSO.
- Users table: search users, see their plans, status, and last activity.
- Subscription management: view active, trialing, canceled subscriptions.
- Feature flags / toggles for experiments or customer-specific options.
- Basic impersonation or “view as user” can be a later improvement.
Environment Separation and Secrets
Even for a starter:
- Separate
.env.local,.env.development,.env.staging,.env.production. - Use different API keys for auth providers, Stripe, and analytics per environment.
- Never hard-code secrets in code or commit
.envfiles. - Use secret storage (Vercel, AWS SSM, 1Password, Doppler, etc.) for production.
- Support multiple deployments referencing the same DB (for staging/preview) without leaking real user data where it shouldn’t go.
Weekend Workflow: From Idea to Billing-Ready SaaS
Let’s structure the weekend into three phases:
- Friday evening: stack and starter choices.
- Saturday: auth and billing wired.
- Sunday: analytics, admin, and production polish.
You can follow this to build from scratch or to integrate a starter like starter into your own workflow.
Friday Evening: Choose Stack and Starter Strategy
Why Next.js Is a Strong Fit for SaaS
Next.js gives you:
- Hybrid rendering: server components and server actions for secure logic, plus client components where needed.
- Built-in routing and middleware for auth gating and rate limiting.
- Easy deployment to Vercel (or others) and good edge support.
- API routes or
approuter handlers for webhooks and backend logic. - Strong TypeScript support and ecosystem integrations.
For a small product team or solo dev, that means:
- Backend and frontend in one codebase.
- Fewer context switches between separate repos/services.
- Faster initial iteration—and easier to template later.
Decide: From Scratch vs. Starter Template
Ask yourself:
- Is this a one-off SaaS, or will I spin up multiple apps over the next year?
- Do I care about building a reusable starter as an asset?
- How much time this weekend can I invest in architecture vs. product features?
You have three approaches:
- Minimal custom build
- Build auth, billing, analytics, admin just for this app.
- Fast for the first app, but harder to reuse later.
- Internal starter
- Build a reusable skeleton you’ll clone for future apps.
- More upfront work this weekend, but saves real time later.
- External starter (e.g.,
starter)- Use a prebuilt Next.js starter that already includes Clerk + billing + analytics + admin.
- Great if you want to ship product this weekend rather than infrastructure.
- You treat the starter as the “internal standard” and align future apps to it.
Criteria for a Good Next.js SaaS Starter Template
If you choose a ready-made template, evaluate it against these criteria:
- Auth:
- Supports an auth provider you’re comfortable with (Clerk, Auth0, etc.).
- Clear route protection patterns and server-side auth APIs.
- Billing:
- Stripe (or equivalent) integration with real subscription states.
- Working example of checkout and customer portal.
- Webhook handler implemented safely (signature verification, idempotency).
- Analytics:
- Event tracking hooks already wired.
- A clear event schema or naming conventions.
- Ability to integrate with your own analytics or hub.
- Admin:
- Prebuilt admin routes and access control.
- User list with key fields (email, role, plan).
- Simple feature flag or settings mechanism.
- Code quality:
- TypeScript, linting, tests (at least smoke tests).
- Sensible folder structure (
approuter,lib,components,modules). - Documentation for setup, local dev, env vars, and deployment.
A starter like starter is designed around this exact checklist: Next.js + Clerk, Stripe-style billing, analytics hooks, and an admin baseline, plus the idea of a “hub” that can track multiple apps.
Friday Deliverables
By the end of Friday, you should have:
- Repo created (or starter cloned).
- Tech choices confirmed:
- Auth provider (e.g., Clerk).
- Billing provider (e.g., Stripe).
- Analytics (third-party or custom).
- Deployment target decided (Vercel is easiest for Next.js).
- An initial
.env.examplesketched with all the keys you’ll need.
Saturday: Wire Auth and Billing

Saturday is about making your starter secure and monetizable.
Step 1: Initialize or Clone the Project
Option A: From scratch (Next.js App Router + TypeScript):
bash
npx create-next-app@latest my-saas
--typescript
--eslint
--app
--src-dir
--import-alias "@/*"
cd my-saas
Option B: Use a template like starter:
bash git clone git@github.com:your-org/starter.git my-saas cd my-saas pnpm install # or yarn / npm
Make sure you can run it locally:
bash pnpm dev
Step 2: Configure Authentication (e.g., Clerk)
Sign up for your auth provider, create an application, and get your keys.
Typical Clerk setup for Next.js App Router:
- Install dependencies:
bash pnpm add @clerk/nextjs
- Add environment variables (local):
env
.env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_...
- Wrap your layout in
<ClerkProvider>and protect routes.
Example app/layout.tsx:
tsx import { ClerkProvider } from "@clerk/nextjs"; import type { ReactNode } from "react";
export default function RootLayout({ children }: { children: ReactNode }) { return ( <ClerkProvider> <html lang="en"> <body>{children}</body> </html> </ClerkProvider> ); }
- Protect a dashboard route:
tsx // app/dashboard/page.tsx import { auth } from "@clerk/nextjs"; import { redirect } from "next/navigation";
export default async function DashboardPage() { const { userId } = auth();
if (!userId) { redirect("/sign-in"); }
return <div>Welcome to your dashboard</div>; }
This pattern is crucial: all sensitive pages and server actions should derive identity from auth() or equivalent, never from client input.
Step 3: Integrate Billing with Stripe
3.1 Create Products and Prices
In the Stripe Dashboard:
- Create a product (e.g., “Pro Plan”).
- Add prices:
- Monthly (e.g.,
$20/month). - Annual (optional, e.g.,
$200/year).
- Monthly (e.g.,
- Note down the
price_xxxIDs.
Decide your plan structure:
FREE: no Stripe subscription.PRO_MONTHLY:price_...monthly.PRO_ANNUAL:price_...annual.
3.2 Add Stripe to Your Codebase
Install Stripe SDK:
bash pnpm add stripe pnpm add -D @types/stripe
Add env vars:
env
.env.local
STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... # from Dashboard (after webhook setup) NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_PRICE_PRO_MONTHLY=price_...
3.3 Create a Checkout Session Handler
With Next.js App Router, create a route handler:
ts // app/api/stripe/checkout/route.ts import Stripe from "stripe"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "@clerk/nextjs";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2023-10-16", });
export async function POST(req: NextRequest) { const { userId } = auth(); if (!userId) { return new NextResponse("Unauthorized", { status: 401 }); }
const { priceId, successUrl, cancelUrl } = await req.json();
// Lookup or create Stripe customer based on your user model const customerId = await getOrCreateStripeCustomerId(userId);
const session = await stripe.checkout.sessions.create({ mode: "subscription", customer: customerId, line_items: [{ price: priceId, quantity: 1 }], success_url: successUrl, cancel_url: cancelUrl, });
return NextResponse.json({ url: session.url }); }
// Example placeholder. Implement in your data layer. async function getOrCreateStripeCustomerId(userId: string): Promise<string> { // 1. Query your DB for an existing customerId // 2. If missing, create Stripe customer and store mapping return "cus_..."; }
This route illustrates the pattern:
- Derive
userIdserver-side via Clerk. - Use
userIdto map tostripe_customer_id. - Create a checkout session with a plan
priceId. - Return the Stripe-hosted
session.urlto the client.
3.4 Implement the Billing Webhook Safely
Webhooks keep your local subscription state in sync with Stripe.
Example app/api/stripe/webhook/route.ts:
ts import Stripe from "stripe"; import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2023-10-16", });
export const runtime = "nodejs"; // ensure Node runtime for webhooks
export async function POST(req: NextRequest) { const body = await req.text(); const sig = headers().get("stripe-signature");
if (!sig) { return new NextResponse("Missing signature", { status: 400 }); }
let event: Stripe.Event;
try { event = stripe.webhooks.constructEvent( body, sig, process.env.STRIPE_WEBHOOK_SECRET! ); } catch (err: any) { console.error("Webhook signature verification failed.", err.message); return new NextResponse("Invalid signature", { status: 400 }); }
try { switch (event.type) { case "customer.subscription.created": case "customer.subscription.updated": case "customer.subscription.deleted": { const subscription = event.data.object as Stripe.Subscription; await syncSubscription(subscription); break; } case "invoice.payment_failed": { const invoice = event.data.object as Stripe.Invoice; await handlePaymentFailure(invoice); break; } default: // Optionally log ignored events break; }
return new NextResponse(null, { status: 200 }); } catch (error) { console.error("Error handling Stripe webhook", error); return new NextResponse("Webhook handler error", { status: 500 }); } }
// Domain functions: keep Stripe-specific logic isolated from your models. async function syncSubscription(subscription: Stripe.Subscription) { const customerId = subscription.customer as string; const status = subscription.status; // active, trialing, past_due, canceled...
const currentPeriodEnd = subscription.current_period_end;
// 1. Find user by stripe_customer_id // 2. Upsert subscription record with status, plan, and timestamps }
async function handlePaymentFailure(invoice: Stripe.Invoice) { const customerId = invoice.customer as string; // 1. Find user // 2. Notify or mark account as needing payment update }
Key safety notes:
- Use
req.text()andconstructEventto verify signatures. - Treat webhooks as idempotent; ensure
syncSubscriptioncan handle duplicates. - Do not trust client side billing state; always treat your DB and webhooks as the source of truth.
Saturday Deliverables
By the end of Saturday:
- Users can sign up and log in via your auth provider.
- Authenticated users can start a Stripe checkout for a subscription.
- Successful checkouts trigger webhooks, and your DB stores subscription state.
- You can see subscriptions in both Stripe and your app’s database.
Sunday: Analytics, Admin, and Production Polish
Sunday is about observability and internal control.
Step 1: Add Product Analytics
Decide your analytics stack:
- Third-party (easier): PostHog, Amplitude, Pirsch, etc.
- Custom: a simple
eventstable + API endpoint.
For a reusable starter:
- Implement a lightweight internal
trackEventfunction. - Optionally forward events to external tools or a hub.
Example event tracking helper:
ts // lib/analytics.ts type EventName = "user_signed_up" | "subscription_activated" | "subscription_canceled";
interface EventPayload { userId: string; properties?: Record<string, any>; }
export async function trackEvent(name: EventName, payload: EventPayload) { // 1. Store in your DB (events table) or send to your hub // 2. Optionally forward to a product analytics provider }
Use this in key flows:
- After successful signup.
- After subscription activation (in webhook handler).
- On cancellation / downgrade.
This gives you consistent, versionable events that multiple apps can share—critical if you want a multi-project hub later.
Step 2: Build a Lightweight Admin Area
Use a protected route like /admin that only admins can access.
With Clerk roles, you might:
tsx // app/admin/layout.tsx import { auth } from "@clerk/nextjs"; import { redirect } from "next/navigation";
export default async function AdminLayout({ children }: { children: React.ReactNode }) { const { userId, sessionClaims } = auth();
if (!userId || sessionClaims?.publicMetadata?.role !== "admin") { redirect("/"); }
return <>{children}</>; }
Then create pages:
/admin/users: list users, their plans, last seen./admin/subscriptions: list subscriptions, statuses./admin/flags: manage feature flags or beta access.
Start simple:
- A table for users with filters.
- A subscription detail view to debug billing issues.
- A form to toggle flags per user or per plan.
This is where a starter like starter can save hours: it typically includes an admin shell, role-based access, and basic views wired to your data models.
Step 3: Basic Observability and Error Tracking
At minimum:
- Centralized logging: structured logs from API routes and webhooks.
- Error tracking: Sentry, Logtail, or similar.
Example Sentry integration:
- Install:
bash pnpm add @sentry/nextjs
- Run init script and configure DSN via env vars.
Then:
- Wrap webhook handlers and critical server actions with try/catch and log errors with context (userId, event type, etc.).
Step 4: Hardening for Production
Focus on a few key areas:
- Rate limiting:
- Apply middleware for login, signup, and webhook-like endpoints.
- Use an in-memory or Redis store (Upstash, etc.) for limit state.
- Environment variables:
- Maintain a strict
.env.examplethat enumerates all required keys. - Validate env vars at startup with a small schema (e.g.,
zod).
- Maintain a strict
- Secrets:
- No secrets committed; use environment-level configuration in your host.
- Rotate keys for auth, Stripe, analytics when moving from dev to prod.
- Role-based access:
- Confirm admin routes are protected and logged.
- Confirm user roles (and flags) are derived from your DB, not from client data.
- Deployment:
- Create
stagingandproductionprojects on Vercel or your host. - Wire separate Stripe and auth projects to each environment if needed.
- Smoke-test checkout and webhooks in staging before production.
- Create
Sunday Deliverables
By the end of Sunday:
- Key product events are tracked (signup, subscription activation, cancellation).
- Admin area exists with at least:
- Users list and subscription status.
- Feature toggle or a simple flags system.
- Error tracking is configured.
- Environment management is defined and documented.
- Staging and production deployments are ready or deployed.
Validating Integration With a Central “Hub”
If you manage multiple SaaS apps or MVPs, a central “hub” that can discover, track, and manage them is valuable:
- Single place to see all signups, revenue, and errors across projects.
- Reusable settings (plans, flags) propagated to each app.
- Ability to de-duplicate infrastructure work across teams.
Your Next.js starter should expose the right hooks to plug into such a hub.
What Is the Hub?
Conceptually, the hub is:
- An internal service that receives and stores events from each app.
- An API that apps use to:
- Register themselves (metadata: name, environment, URL, features).
- Push usage and billing events.
- Pull configuration (feature flags, experiments).
It might be another Next.js app, a Node service, or even a set of serverless functions.
How Your Starter Should Integrate With the Hub
Your starter template can integrate with the hub in a few simple ways:
app registration endpoint: on startup or deployment, the app calls a hub endpoint likePOST /api/projects/registerwith:projectId(or repo name).baseUrl(staging/production).authProviderinfo.billingProviderinfo.plansmetadata.
event forwarding: yourtrackEventfunction forwards events to the hub:name,userId,projectId,properties,timestamp.- The hub normalizes and aggregates events across apps.
subscription sync: when Stripe webhooks update local subscription state, you emit events that the hub consumes:subscription_activated,subscription_canceled, etc.- The hub can compute MRR, churn, etc., across apps.
Example Hub Event Forwarding Workflow
- User signs up in your app:
- Auth provider creates a user.
- Your app persists a
Userrow. - You call
trackEvent("user_signed_up", { userId }). trackEventwrites locally and POSTs toHUB_URL/api/events.
- User upgrades to Pro:
- Stripe checkout completes, webhook fires.
syncSubscriptionsetsstatus = "active"andplan = "PRO".trackEvent("subscription_activated", { userId, properties: { plan: "PRO" } }).- Hub receives the event and shows it on a revenue dashboard.
- You want to verify:
- Go to the hub UI.
- Filter by project or environment.
- Confirm that your test user appears with the correct plan and events.
Quick Test Flow to Validate Starter ↔ Hub
- Configure
HUB_URLand any API keys in your starter’s.env. - Run your app locally pointing to a dev instance of the hub.
- Step through:
- Create a test user (sign up).
- Start a trial or subscribe to a test plan in Stripe.
- Cancel the subscription.
- In the hub:
- Confirm
user_signed_up,subscription_activated, andsubscription_canceledappear. - Check that the associated metadata (email, plan, environment) looks correct.
- Confirm
A starter like starter can bake in this hub integration:
- A standard event schema shared across projects.
- Pre-configured hub endpoints and keys.
- Admin views that reflect hub status, not just local state.
Common Pitfalls and Best Practices

Pitfall 1: Rebuilding the Same Infrastructure for Every App
Symptoms:
- Each new SaaS has its own way of storing users, plans, flags.
- Webhooks are implemented differently every time.
- You can’t easily answer cross-project questions like “How many active subscribers does our portfolio have?”
Best practices:
- Treat auth + billing + analytics + admin as a reusable module.
- Standardize folder structures and naming conventions.
- Build or adopt a starter once and reuse it across projects.
Pitfall 2: Messy Environment and Secret Management
Symptoms:
- You only have
devandprod, or worse, onlyprod. - A staging environment reuses production Stripe keys.
- Secrets live in
.env.localfiles scattered across laptops.
Best practices:
- Maintain
.env.exampleas part of your starter. - Use distinct envs per stage (
LOCAL,DEV,STAGING,PROD). - Use your host’s secret manager for production.
- Automate env var validation on app start (simple runtime checks).
Pitfall 3: Tangled Auth and Billing Logic
Symptoms:
- Stripe webhooks directly manipulate auth provider data.
- User roles and plans are mixed in awkward ways.
- Hard to swap auth provider or billing later.
Best practices:
- Establish clear domain models:
User(identity and profile).Subscription(billing, plan, status).FeatureFlag(capabilities).
- Keep third-party SDK logic in boundary modules (e.g.,
lib/auth,lib/billing). - Make your domain layer depend on these modules, not the other way around.
Pitfall 4: Inconsistent Analytics Events
Symptoms:
- Every app names events differently (
user_signup,signedUp,signup_user). - You can’t combine or compare data across projects.
- Event shape changes silently.
Best practices:
- Define a shared event schema in the starter.
- Use a small enum or union type for event names and centralize the tracking helper.
- Version your events if you need to change properties.
- Document event names and when to fire them.
How a Ready-Made Starter Can Shortcut This Work
Everything in this article can be built in a weekend, but it’s still a lot:
- Auth configuration and route protection.
- Stripe integration and safe webhooks.
- Admin UI and role management.
- Analytics event pipeline and hub integration.
- Environment setup and production hardening.
A well-designed starter like starter aims to collapse that into hours:
- Next.js + Clerk baseline:
- Auth wiring, protected routes, and role support are already done.
- Billing integration:
- Stripe (or similar) checkout and webhook handlers are implemented with a reusable subscription model.
- Analytics hooks:
- A standardized
trackEventhelper and event schema exist from day one.
- A standardized
- Admin area:
- Users, subscriptions, and flags views are prebuilt, behind admin auth.
- Hub integration:
- The starter exposes the endpoints and event forwarding needed to plug into a central hub, so you can see all your apps in one place.
Using starter in Practice
A realistic workflow using starter:
- Clone the
starterrepo and configure env vars for Clerk, Stripe, and your hub. - Deploy to staging (e.g., Vercel) and run through:
- Sign up.
- Subscribe with test card.
- Cancel.
- Verify:
- Users and subscriptions appear in the starter’s admin.
- Events show up correctly in your hub.
- Fork or duplicate the project for each new SaaS:
- Keep core auth/billing/analytics/admin aligned.
- Focus per-project effort on domain features, not infrastructure.
The same principles apply if you roll your own internal starter. The key is to treat this as infrastructure you invest in once and reuse many times, rather than redoing “auth + billing + analytics + admin” per project.
Wrap-Up
A production-ready Next.js SaaS starter isn’t just a login page and some Stripe buttons. It’s a small, coherent platform with:
- Auth and user management.
- Billing and subscriptions with robust webhooks.
- Analytics and event tracking.
- Admin tools for operations.
- Clean environment and secret handling.
- Optional integration with a central hub to manage many apps.
You can build this in a focused weekend if you follow a structured workflow: choose your stack Friday, wire auth and billing Saturday, and finish analytics, admin, and hardening on Sunday. Or you can adopt a starter like starter that pre-solves most of these problems and gives you a consistent baseline for every new SaaS you ship.
Either way, the goal is the same: stop rebuilding the same plumbing and start spending more of your weekends on the product ideas that actually differentiate your apps.
