DEV Community

seller-mind
seller-mind

Posted on

How I Stopped Rewriting Auth and Billing for Every New SaaS

When I first started building micro-SaaS products, the initial rush of a new idea was always quickly followed by a heavy sigh. I’d sketch out the database, plan the UI, and then hit the inevitable wall: the plumbing.

Setting up Stripe subscriptions, configuring NextAuth with multiple OAuth providers, and wiring up webhooks wasn't just tedious; it was a massive context switch. I found myself spending the first three days of every project just recreating the same foundational code I’d written a dozen times before.

I realized I was spending more time maintaining boilerplate than actually validating my product ideas. So I decided to build something.

The "Boilerplate Tax"

We’ve all been there. You have a great idea for a SaaS, but before you can write a single line of business logic, you have to build the engine.

The problem with this "boilerplate tax" isn't just the lost time—though losing a weekend to Stripe webhook testing is painful. The real cost is the fragility of the result. When you rush through setting up authentication or billing just to get to the "fun" part, you inevitably leave edge cases unhandled. A missed webhook event can mean a user gets premium access without paying, or worse, gets locked out after a valid renewal.

Furthermore, as the ecosystem shifts—like the migration to the Next.js App Router—keeping those personal boilerplates updated becomes a part-time job. I needed a way to start projects with production-ready foundations without having to maintain them myself.

Architecting the Solution

I wanted to build a starter kit that handled the heavy lifting, specifically focusing on the modern stack I was using: Next.js 14 with the App Router, TypeScript, and edge computing.

The biggest architectural decision was incorporating Cloudflare Workers for edge-side API routes. While standard serverless functions are great, running certain tasks at the edge drastically reduces latency for global users. This was especially important for the built-in SEO tools I wanted to include.

Let’s look at how the edge SEO analysis works. Instead of sending a URL to a central server, parsing the DOM, and sending it back, the Cloudflare Worker intercepts the request, fetches the target page at the edge, and runs lightweight regex checks.

Here is a simplified version of the worker logic:

// 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 keeping the parsing regex-based and avoiding heavy DOM libraries, the worker stays incredibly lightweight. The cf cache configuration also ensures that if multiple users check the same URL within a five-minute window, we serve the cached response, saving compute time.

On the billing side, I went with a strict, type-safe approach to Stripe. The Next.js App Router route handlers make it straightforward, but handling the webhook signatures securely was a priority. I ensured the starter kit includes the exact webhook verification logic needed to prevent spoofed events, mapping Stripe events directly to local database updates.

What Worked and What Surprised Me

Building this out into a cohesive starter kit (which I call SaaSLaunch) taught me a lot about what actually matters when shipping.

What worked exceptionally well was the one-click deployment setup. Being able to deploy to either Vercel or Cloudflare Pages without tweaking environment variables manually every time removed a lot of friction. The TypeScript-first approach also paid off; catching a mismatched Stripe price ID type during development saved me from a potentially embarrassing checkout bug.

What surprised me was how much time the pre-configured NextAuth setup saved. OAuth providers all have slight quirks in their callback URLs and token refreshes. Having those abstracted away meant I could just drop in my client IDs and move on.

If I could change one thing from the initial build, it would be the database schema. I initially included a highly normalized schema with multiple relational tables. In practice, for early-stage SaaS, a slightly flatter schema is easier to iterate on. I’ve since simplified the default models in the kit to be more pragmatic.

Putting It to the Test

The edge computing approach for the SEO tools ended up being my favorite part of the project. It’s fast, it’s cheap to run, and it provides immediate value without requiring a complex infrastructure setup.

If you’re currently working on a SaaS landing page and want to see how it holds up from an SEO perspective, I’ve extracted that edge worker into a standalone tool. You can run your URL through it to check your title tags, meta descriptions, and H1 counts instantly.

You can try the free SEO Checker here: https://saaslaunch.dev/free-tools/seo-checker

It’s a small tool, but it reflects the same philosophy as the rest of the kit: keep it fast, keep it simple, and focus on what actually helps you ship.


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

Top comments (0)