DEV Community

Cover image for I Built a Production-Ready Node.js SaaS Boilerplate So You Don't Have To
joonlee22
joonlee22

Posted on

I Built a Production-Ready Node.js SaaS Boilerplate So You Don't Have To

Every time I started a new SaaS project, I spent the first 3 days building the same things.

Auth. Billing. Database setup. Deployment config. Every. Single. Time.

So I stopped. I packaged everything into a production-ready boilerplate and I'm never doing it again.

Here's exactly what I built and how it works.

The Stack

  • Backend: Node.js + Express
  • Frontend: Next.js 14 + Tailwind CSS
  • Database: PostgreSQL + Prisma ORM
  • Auth: JWT (access + refresh tokens) + Google OAuth
  • Billing: Stripe subscriptions
  • Deployment: Railway (backend) + Vercel (frontend)

Auth — Done Right

Most tutorials show you JWT auth but skip the details that matter in production:

  • Access tokens expire in 7 days
  • Refresh tokens are stored in the database and rotated on every use — if a token is stolen and used, the original is invalidated
  • Google OAuth creates or links accounts automatically
  • bcrypt with 12 salt rounds for password hashing
  • Rate limiting on auth routes (10 requests per 15 minutes) to block brute force attacks
// Refresh token rotation — prevents reuse after theft
const rotateRefreshToken = async (rawRefresh) => {
  const stored = await prisma.refreshToken.findUnique({
    where: { token: rawRefresh },
    include: { user: true },
  });

  if (!stored || stored.expiresAt < new Date()) return null;

  // Delete old token before issuing new one
  await prisma.refreshToken.delete({ where: { id: stored.id } });

  return issueTokens(stored.user);
};
Enter fullscreen mode Exit fullscreen mode

Stripe Billing — The Parts Nobody Explains
Stripe docs are good but they don't show you how to wire everything together in a real app. Here's what I set up:

Checkout Sessions — user clicks upgrade, gets redirected to Stripe, comes back subscribed
Customer Portal — one line of code lets users manage, upgrade, downgrade, or cancel themselves
Webhooks — subscription changes sync to your database automatically
Plan-gated routes — lock API endpoints behind a minimum plan level

// Lock a route behind a plan — one line
router.get('/analytics', requireAuth, requirePlan('STARTER'), handler);
Enter fullscreen mode Exit fullscreen mode

The webhook handler covers all the important events:

customer.subscription.created — new subscriber
customer.subscription.updated — plan change
customer.subscription.deleted — cancellation, auto-downgrades to FREE
Database Schema
Three models — kept it simple:

User — email, password hash, Google ID, Stripe customer ID
Subscription — plan, status, billing period, Stripe subscription ID
RefreshToken — token, user, expiry (enables token rotation and logout-all-devices)
Deployment
Both services have config files included:

railway.json for the backend
Vercel auto-detects Next.js
The backend handles graceful shutdown on SIGTERM — Railway and Render send this signal on every deploy, so in-flight requests finish cleanly before the process exits.

Live Demo
You can see the full frontend running here:
https://saas-boilerplate-hsh5xrikl-johnolee15-9278s-projects.vercel.app

Landing page, auth, dashboard, billing — all working.

Get the Code
I packaged this up and listed it on Gumroad for $49:
👉 https://looneyjoons.gumroad.com/l/nodejs-saas-boilerplate

Includes the full backend + frontend source, README with step-by-step setup, and .env.example with every variable documented.

If you have any questions about the stack or implementation drop them in the comments — happy to help.

Top comments (0)