DEV Community

seller-mind
seller-mind

Posted on

How I Architected My Next.js SaaS Using Edge Workers

When I first started building SaaS products, I spent more time wrestling with infrastructure than actually writing features. I’d spin up a new project, set up a basic Next.js app, and immediately hit a wall when trying to handle global traffic, manage Stripe webhooks, and keep authentication secure. Every tutorial I found was either too basic or assumed I wanted to manage a complex Kubernetes cluster. I just wanted to ship a product without spending weeks configuring servers. So I decided to build something.

The core problem with building a SaaS from scratch isn't just writing the code; it's making the right architectural decisions early on. If you ignore infrastructure patterns, you end up with a fragile codebase. For example, if you put all your API routes in a single region, your global users experience annoying latency. If you don't structure your Stripe integration correctly from day one, you'll face billing desyncs that are a nightmare to untangle later. The cost of ignoring these architectural details isn't just technical debt—it's developer burnout and a product that struggles to scale when you finally get traction.

To solve this, I had to make some deliberate architecture choices. I wanted a stack that was modern, type-safe, and optimized for both developer experience and end-user performance.

I chose Next.js 14+ with the App Router for the core application. It gives me React Server Components out of the box, which drastically reduces the client-side JavaScript bundle. For authentication, NextAuth.js with OAuth providers handled the heavy lifting securely, saving me from building custom session management. For billing, I integrated Stripe subscription flows, ensuring checkout sessions and webhook handlers were strictly typed with TypeScript.

But the biggest architectural decision was moving specific, globally-accessed workloads to Cloudflare Workers. Instead of routing every API request through a centralized Node.js server, I pushed lightweight API routes to the edge.

A perfect example of this is the SEO analyzer tool I built. Instead of fetching a target URL from a central server (which adds latency for users far from the server), the Cloudflare Worker fetches the target page at the edge, right next to the user.

Here is what that edge function looks like:

// 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 the cf cache properties, we can cache the fetch at the edge for a few minutes, making subsequent checks for the same URL virtually instant. The regex-based parsing keeps the worker lightweight, avoiding the need to load heavy DOM parsing libraries in an edge environment.

For the core SaaS billing, I kept the Stripe checkout logic in the Next.js App Router API routes, as it requires secure server-side environment variables and direct database access for user mapping.

// 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

Separating the workloads this way has been incredibly effective. The Next.js app handles the complex UI and database interactions, while the Cloudflare Workers handle the globally distributed, stateless tasks.

What surprised me the most was how seamless the deployment process became. By utilizing one-click deployments to Vercel for the Next.js app and Cloudflare Pages for the edge workers, I didn't have to write complex CI/CD pipelines to get things running. The TypeScript-first approach also paid off massively; catching type mismatches between the frontend and the edge workers before deployment saved me hours of debugging.

If I were to change one thing, I might eventually swap the regex-based HTML parsing in the worker for a more robust streaming HTML parser, but for an MVP, the regex approach keeps the worker execution time well under the millisecond limits.

I ended up packaging all of these architectural decisions, along with the Next.js 14 App Router setup, Stripe integration, and NextAuth configuration, into a boilerplate called SaaSLaunch. It’s designed to give you a production-ready starting point so you can focus on your actual product features rather than reinventing the wheel.

If you want to see the edge SEO worker in action, I've deployed it as a free tool. You can test it out 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)