I still remember the third time I tried to launch a side project. I had the idea, I had the UI mockups, and I was excited to start coding. But the moment I had to wire up Stripe webhooks, configure NextAuth session callbacks, and set up the database schema, my motivation tanked. I spent three weeks just configuring boilerplate instead of building actual features. I was jumping between Stripe docs, NextAuth guides, and Vercel deployment settings, losing my flow every time I context-switched. By the time the infrastructure was finally "done," I was too burned out to write the core product logic.
So I decided to build something.
The Problem
The problem isn't that building a SaaS is technically impossible. The problem is the repetitive glue code and the context switching it demands. Almost every SaaS needs authentication, subscription billing, and a performant landing page. But stitching these together from scratch means jumping between different documentation ecosystems.
You're configuring Stripe checkout sessions in one tab, wrestling with OAuth provider callbacks in another, and trying to optimize API response times in a third. The cost of ignoring this fatigue is high. It leads to delayed launches, buggy webhook handlers, and ultimately, abandoned projects. When you spend 80% of your time on infrastructure and 20% on your actual unique value proposition, the math just doesn't work out for indie hackers. Developers shouldn't have to reinvent the wheel every time they want to test a new idea.
Technical Approach
I wanted a TypeScript-first stack that handled the boring stuff out of the box, allowing me to focus on the product. I chose Next.js 14 with the App Router for the frontend and API routes, NextAuth.js for OAuth, and Stripe for billing.
Setting up the Stripe checkout flow in the Next.js App Router was straightforward, but it required careful handling of environment variables and session states. Here is how the API route for creating a Stripe checkout session looks in the boilerplate:
// 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 });
}
This handles the checkout creation, but the real architectural decision I made was adding Cloudflare Workers to handle edge-side tasks. Specifically, I wanted to build a built-in SEO checker and landing page analyzer without bloating the main Next.js server.
By offloading the SEO analysis to Cloudflare Workers, the processing happens at the edge, close to the user. Here is the edge worker that powers the SEO analysis tool:
// 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' },
});
},
};
The trade-off here was using regex for HTML parsing instead of a full DOM parser like jsdom. Since this runs on the edge, keeping the bundle size small and execution time minimal was critical. Regex is usually frowned upon for HTML parsing, but for simple, predictable extraction of title, meta description, and H1 counts on standard landing pages, it works perfectly and keeps the worker lightweight.
Practical Results
The biggest win was the one-click deployment to Vercel and Cloudflare Pages. Being able to clone the repository, add my environment variables, and have a fully functioning SaaS with Stripe checkout and edge tools in under ten minutes was incredibly freeing. It completely eliminated the "setup phase" burnout.
What surprised me was how much faster the edge SEO checker performed compared to a traditional serverless function. Running the HTML fetching and parsing on Cloudflare Workers meant the latency was practically non-existent for global users, and the cf cache properties prevented us from hammering the target server with repeated requests.
If I were to do it again, I'd probably add more granular webhook retry logic out of the box. Stripe's retry mechanisms are great, but debugging failed webhooks locally during development can still be tricky without a robust local tunneling setup. I also realized that while regex works for 95% of landing pages, adding a fallback to a lightweight parser for highly malformed HTML would make the tool more resilient.
Try the Edge SEO Tool
If you're working on a SaaS and want to see how the edge SEO analysis works in practice, I've extracted that exact logic into a free tool. You can test your landing pages directly at the SaaSLaunch free SEO checker. It runs the same edge-based analysis to check your titles, meta descriptions, and H1 tags, giving you a quick health check without you having to deploy anything yourself.
Full disclosure: I'm the developer of SaaSLaunch. I built this because I needed it myself.
Top comments (0)