DEV Community

seller-mind
seller-mind

Posted on

Building an Edge-First SaaS Boilerplate with Next.js and Cloudflare

I still remember the exact moment I realized I was spending 80% of my time building the same boilerplate over and over. It was a Tuesday night, and I was on my third cup of coffee, trying to debug a Stripe webhook that kept failing in production. I had already spent the whole weekend setting up NextAuth, configuring the database schema, and writing the same API routes I’d written a dozen times before. I wasn't building my actual SaaS idea; I was just building the plumbing. I was exhausted, frustrated, and ready to quit. So I decided to build something to fix this exact problem.

The "SaaS boilerplate" problem is a massive time sink for indie developers. Every time you start a new project, you have to make the same architectural decisions: Which auth provider? How do I handle Stripe subscriptions securely? Where should I host the edge functions? If you get these wrong, you end up with technical debt that slows you down later. But if you spend too much time perfecting them upfront, you delay your actual launch. The cost of ignoring this bottleneck isn't just lost time; it's lost momentum and mental energy. When you spend three weeks just setting up billing and auth, your motivation drops. You start doubting the idea before you've even written a single line of core business logic. We need a way to abstract the repetitive infrastructure so we can focus on the unique value our software provides.

When I started designing SaaSLaunch, I wanted to use a modern, type-safe stack. I chose Next.js 14+ with the App Router because it handles server components beautifully and keeps the bundle size small. For the edge layer, I integrated Cloudflare Workers. Why Cloudflare? Because running API routes at the edge significantly reduces latency for global users, which is crucial for tools that need to fetch and parse external data quickly.

Let's look at how I handled the Stripe checkout integration in the Next.js App Router. The key here is keeping the secret key secure on the server while passing the necessary context to the client.

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

Notice how we use client_reference_id to map the Stripe session back to our internal user ID. This is critical for the webhook handler later on. When Stripe sends the checkout.session.completed event, we use that reference ID to update our database and provision the user's subscription without needing a complex lookup.

Now, let's look at the edge side. I built a Cloudflare Worker to handle the SEO analysis. Running this at the edge means we can fetch external websites with very low 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 Cloudflare's cf object, we can set cache rules directly in the fetch request. I chose to parse the HTML using simple regex instead of a heavy DOM parser like Cheerio. Since we only need basic meta tags and heading counts, regex keeps the worker execution time minimal and well within the free tier limits.

The biggest win was how much faster I could spin up new projects. Having NextAuth and Stripe pre-configured with environment variables and webhook handlers saved me a significant amount of time per project. What surprised me the most was the Cloudflare Workers integration. I initially thought edge computing would be overkill for a simple SaaS starter, but the latency improvements for the SEO checker tool were very noticeable. If I could change one thing, I would probably add more granular role-based access control (RBAC) out of the box. Right now, it handles basic user authentication perfectly, but complex team workspaces require a bit more custom setup depending on your specific needs.

If you want to test out the edge SEO analysis logic I mentioned, I've extracted it into a standalone tool. You can use the free SEO Checker at https://saaslaunch.dev/free-tools/seo-checker to analyze your landing pages and see how the edge parsing works in practice. It's completely free and doesn't require an account to use.


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

Top comments (0)