Stop 19 · Build Full-Stack Portfolio ProjectsProjects · 1/12

SaaS Starter Kit

Part of the Full Stack Developer Roadmap

In short

Every SaaS needs the same boring parts: sign up, teams, billing, settings, emails. Build them once, properly, and you have a base for every idea you ship after this.

A reusable foundation covering auth, billing, and a dashboard shell — the kind of boilerplate real SaaS products are built on top of, and a great demonstration of full-stack fundamentals done well.

The purple blob opens the door to a furnished room carrying only a small suitcase.

Think of it like this

A fully furnished PG room

A good PG gives you a bed, Wi-Fi, a lock and a study table on day one. You only bring your suitcase. A starter kit is that room for a product: auth, billing and a dashboard ready, so you just bring the idea.

Watch it happen

Build plan for the starter kit

Auth
Teams
Billing
Dashboard
Emails
Deploy

1/6Email plus Google sign in, password reset and sessions in httpOnly cookies. Every later part depends on this.

The big ideas

What you need to know

Auth you trust

Sign up, login, reset, logout and session expiry all work, and every protected route checks the session on the server.

Billing from webhooks

The plan in your database changes only when Stripe's signed webhook says so, never from a redirect URL.

Teams from day one

Put billing and data on the team, not the user. Adding teams later means rewriting half the app.

A README that sells it

Explain setup in five commands and show a demo login. That's what recruiters actually open.

See it in code

Walk through the code

ts
1import Stripe from "stripe";2const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);3export async function POST(req: Request) {4  const body = await req.text(); // raw text: the signature covers the exact bytes5  const sig = req.headers.get("stripe-signature") ?? "";6  let event: Stripe.Event;7  try {8    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);9  } catch { return new Response("Bad signature", { status: 400 }); }10  if (event.type.startsWith("customer.subscription.")) {11    const sub = event.data.object as Stripe.Subscription;12    const plan = sub.status === "active" || sub.status === "trialing" ? "pro" : "free";13    await db.team.update({ where: { stripeCustomerId: String(sub.customer) }, data: { plan } });14  }15  return new Response(null, { status: 200 });16}

1/3Verify the signature on the raw body. A fake request from anyone else fails here with 400.

A Next.js route handler at app/api/stripe/webhook/route.ts. Stripe tells your server when a subscription changes, and only a correctly signed event can change a team's plan.

Save yourself the pain

Common mistakes

Oops

Upgrading the user when they land on /success after checkout.

Do this instead

Anyone can open that URL. Upgrade only from the verified webhook, and show 'processing' until it arrives.

Oops

Building ten half-done features into the kit.

Do this instead

Keep it small and solid: auth, teams, billing, settings. Every part tested and documented.

Your mission

First milestone: paid plan works

~45 min

Quick check

Why should a SaaS update the plan from Stripe's webhook rather than the success page redirect?

Go deeper

  1. 1Include working signup/login, a protected dashboard, and Stripe subscription billing.
  2. 2Make it genuinely reusable — clean environment config, clear folder structure.
  3. 3Document how someone else could clone it and start a new project from it.

More projects at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge