I still remember the exact moment I realized I was spending more time configuring boilerplate than actually building my product. It was 2 AM, and I was debugging a Stripe webhook that kept failing because my local tunnel had dropped. Again.
When I first started building SaaS products, I thought the hard part was the core feature. But the reality? The hard part is wiring up authentication, subscription billing, and edge deployments from scratch every single time. I was burning out before I even wrote a line of business logic. So I decided to build something.
The Cost of Reinventing the Wheel
The problem with building SaaS apps from scratch isn't just the initial setup time. It's the cognitive load of context switching. Every time you start a new project, you have to remember how to configure NextAuth with OAuth providers, set up Stripe checkout sessions, handle webhook idempotency, and optimize your API routes for global latency.
If you ignore this and just hack it together, you end up with a fragile codebase that breaks when you scale. You spend weeks reinventing the wheel instead of solving your users' actual problems. The cost of ignoring a solid foundation is delayed launches and technical debt that compounds daily. I needed a way to standardize my stack without sacrificing flexibility.
Architecting the Stack
I wanted a foundation that was modern, type-safe, and globally distributed. I chose Next.js 14 with the App Router for the frontend and API routes, NextAuth.js for authentication, and Stripe for billing. But the real game-changer for me was moving specific API routes to Cloudflare Workers.
By pushing things like SEO analysis to the edge, I could drastically reduce latency for global users. Let's look at how I built the edge SEO analyzer. Instead of routing a request to a central server, we intercept it at the edge, fetch the target HTML, and run regex-based checks without needing a heavy DOM parser.
// 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' },
});
},
};
This worker checks the pathname, extracts the target URL from the query params, and fetches the HTML using Cloudflare's cache rules to keep latency low. Then, it uses simple regex to extract the title, meta description, and H1 count. It's lightweight, fast, and runs in milliseconds close to the user.
For the core app billing, I leaned heavily into TypeScript to ensure type safety across the Stripe payloads. When creating a checkout session in the Next.js App Router, passing the client_reference_id is crucial for mapping the Stripe session back to your internal database user.
// 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 });
}
Practical Results and Trade-offs
The biggest win was the one-click deployment. Being able to push the Next.js frontend to Vercel and the edge workers to Cloudflare Pages in a single step saved me hours of CI/CD configuration.
What surprised me was how much I relied on the built-in SEO tools. Having a landing page analyzer right inside the dashboard helped me quickly audit my own marketing pages without leaving the app environment.
One trade-off I navigated was the difference between Vercel serverless functions and Cloudflare Workers. While Vercel is great for Next.js rendering, Cloudflare Workers have a much faster cold start time, which is why I offloaded the heavy HTML fetching logic there. If I were to do this again, I'd probably add more granular webhook retry logic out of the box, as Stripe's default retries are good but custom idempotency keys are always better for complex state changes.
Try the Edge SEO Tool
If you're working on a SaaS and need to quickly audit your landing pages before launch, I extracted the edge SEO logic into a standalone free tool. You can check your page's title tags, meta descriptions, and heading structure without signing up for anything at the SEO Checker.
Full disclosure: I'm the developer of SaaSLaunch. I built this because I needed it myself.
Top comments (0)