DEV Community

seller-mind
seller-mind

Posted on

How I Built an Edge-Ready SaaS Boilerplate with Next.js and Stripe

When I first started building SaaS products, I fell into the same trap every time. I’d have a great idea, spin up a new repository, and immediately get bogged down in the plumbing. I was struggling with wiring up authentication, configuring Stripe webhooks, and figuring out how to handle edge-side API routes without adding massive latency. By the time I got the boilerplate working, my motivation for the actual product was halfway gone. I realized I was spending 80% of my time on infrastructure and only 20% on the unique value of my app. So I decided to build something to fix this for myself.

The problem with starting a SaaS from scratch isn't the core logic; it's the repetitive integration work. Every project requires authentication, subscription billing, and a way to serve assets or run lightweight APIs globally. If you don't have a solid foundation, you end up patching things together. The cost of ignoring this is high. You ship a fragile MVP that breaks when you try to scale, or you spend weeks just getting the checkout flow to work reliably. You need a setup that handles the boring stuff securely and efficiently, so you can focus on the features that actually matter to your users.

I wanted to create a boilerplate that was modern, type-safe, and leveraged edge computing. I chose Next.js 14 with the App Router for the frontend and standard API routes, NextAuth.js for OAuth, and Stripe for billing. I also made sure the entire codebase is TypeScript-first, providing full type safety from the database queries up to the frontend components. But the real game-changer for me was integrating Cloudflare Workers for edge-side tasks.

Let's look at how I handled the Stripe checkout using the Next.js App Router. It's crucial to keep this secure and simple.

// Next.js API route for Stripe checkout (App Router)
import { NextResponse } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const { priceId, userId } = await req.json();

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.APP_URL}/dashboard?success=true`,
    cancel_url: `${process.env.APP_URL}/pricing?cancelled=true`,
    client_reference_id: userId,
  });

  return NextResponse.json({ url: session.url });
}
Enter fullscreen mode Exit fullscreen mode

This route takes the price ID and user ID, creates a Stripe Checkout session, and returns the URL to redirect the user. It's straightforward, but getting the environment variables and success/cancel URLs right takes the headache out of billing.

Where things get interesting is the edge computing side. I needed a way to analyze landing pages for SEO without routing all that traffic back to a central server. I built a Cloudflare Worker to handle this. Because it runs at the edge, it fetches the target URL from the closest geographic location, drastically reducing latency.

// Cloudflare Worker for edge SEO analysis
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname !== '/api/analyze') {
      return new Response('Not found', { status: 404 });
    }

    const targetUrl = url.searchParams.get('url');
    if (!targetUrl) {
      return new Response(JSON.stringify({ error: 'Missing url' }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' },
      });
    }

    // Fetch target page at the edge (low latency worldwide)
    const response = await fetch(targetUrl, {
      cf: { cacheTtl: 300, cacheEverything: true },
    });
    const html = await response.text();

    // Run SEO checks (regex-based, no DOM needed)
    const title = html.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] || '';
    const desc = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["']/i)?.[1] || '';
    const h1Count = (html.match(/<h1[ >]/gi) || []).length;

    return new Response(JSON.stringify({ title, desc, h1Count }), {
      headers: { 'Content-Type': 'application/json' },
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

By using regex to parse the HTML directly in the Worker, I avoid the overhead of a full DOM parser, keeping the Worker lightweight and fast. The cf cache properties also ensure we aren't repeatedly fetching the same target page if multiple users check it within the TTL window.

Building this architecture taught me a lot about the trade-offs in modern SaaS development. What worked really well was the combination of Next.js App Router and Cloudflare Workers. The type safety across the entire stack meant fewer runtime errors, and the 1-click deployment to Vercel or Cloudflare Pages made iterating much faster. The built-in SEO checker and landing page analyzer tools have been particularly helpful for validating my own projects before launch.

What surprised me was how much of a difference the edge routing made for the SEO tools. Fetching and analyzing pages at the edge felt instantaneous compared to my previous serverless setups. If I were to change anything, I might look into abstracting the Stripe webhook handling into a more generic event emitter, just to keep the route handlers completely focused on the request/response cycle.

If you're tired of rebuilding the same authentication and billing flows for every new project, or if you just want to test how your site performs, I've open-sourced the core of this setup as SaaSLaunch. It includes the Next.js boilerplate, Stripe integration, and the edge SEO tools.

You can try out the built-in SEO checker and landing page analyzer for free right here: https://saaslaunch.dev/free-tools/seo-checker


Full disclosure: I'm the developer of SaaSLaunch. I built this because I needed it myself.

Top comments (0)