<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: konszogr</title>
    <description>The latest articles on DEV Community by konszogr (@konszogr).</description>
    <link>https://dev.to/konszogr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024037%2F9b33b302-26b1-4bf6-96e9-9fe76a54e22e.png</url>
      <title>DEV Community: konszogr</title>
      <link>https://dev.to/konszogr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/konszogr"/>
    <language>en</language>
    <item>
      <title>I reviewed my own SaaS starter kit like a hostile buyer before selling it. It didn't all pass</title>
      <dc:creator>konszogr</dc:creator>
      <pubDate>Fri, 10 Jul 2026 16:19:16 +0000</pubDate>
      <link>https://dev.to/konszogr/i-reviewed-my-own-saas-starter-kit-like-a-hostile-buyer-before-selling-it-it-didnt-all-pass-32b2</link>
      <guid>https://dev.to/konszogr/i-reviewed-my-own-saas-starter-kit-like-a-hostile-buyer-before-selling-it-it-didnt-all-pass-32b2</guid>
      <description>&lt;p&gt;Most starter kits are sold on a README promise. A list of green checkmarks, a slick landing page, and the quiet assumption that you'll never actually audit the code before your card gets charged.&lt;/p&gt;

&lt;p&gt;I didn't trust my own kit enough to do that. So before I put a price on it, I sat down and reviewed &lt;strong&gt;NuxtForge&lt;/strong&gt; the way a skeptical buyer would: fresh clone, README only, no mercy, no memory of having written any of it.&lt;/p&gt;

&lt;p&gt;It did not all pass. This is what broke, what I fixed, and what's actually tested now — because a kit that hides its history will hide its bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is, in one paragraph
&lt;/h2&gt;

&lt;p&gt;NuxtForge is a decoupled &lt;strong&gt;Nuxt 4 + NestJS 11&lt;/strong&gt; SaaS foundation in TypeScript. Not a monolith — a real API (NestJS, Prisma 6, PostgreSQL 17, Redis, BullMQ, S3/MinIO, Stripe, Resend) and a separate SSR frontend (Nuxt 4) behind Nginx, all wired together with Docker Compose. The idea is boring on purpose: give you the undifferentiated plumbing every SaaS rebuilds from scratch — auth, tenant isolation, config, billing hooks, an admin shell — already built and tested, so you start on &lt;em&gt;your&lt;/em&gt; product instead of on &lt;code&gt;npm init&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the pitch. Now the part nobody shows you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hostile review: four things that broke
&lt;/h2&gt;

&lt;p&gt;I cloned it into an empty folder, opened only the README, and started following it like someone who had just paid for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The test runner was dead.&lt;/strong&gt; There was a &lt;code&gt;test&lt;/code&gt; script in &lt;code&gt;package.json&lt;/code&gt; — and no test configuration behind it. The suite couldn't even run. A kit that advertises "tested" with a test command that does nothing is exactly the kind of thing you only discover &lt;em&gt;after&lt;/em&gt; you've paid. Fixed: a real suite now covers tenant isolation, webhook idempotency, auth, and permissions — including the failure paths, because "denies when it should" matters more than "allows when it should."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A fresh clone didn't boot.&lt;/strong&gt; Four separate bugs on the happy path, each individually small, collectively fatal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In-container service hosts pointing at &lt;code&gt;localhost&lt;/code&gt; instead of the compose service names.&lt;/li&gt;
&lt;li&gt;A dev command the container entrypoint quietly ignored.&lt;/li&gt;
&lt;li&gt;Env values silently corrupted by trailing &lt;code&gt;# comments&lt;/code&gt; that got parsed as part of the value.&lt;/li&gt;
&lt;li&gt;An SSR self-fetch loop that spiralled into an out-of-memory 504.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these show up in a screenshot. All of them show up the first time a buyer runs &lt;code&gt;docker compose up&lt;/code&gt;. Fixed: a clean clone now boots to a working admin login.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Nobody could log in.&lt;/strong&gt; The seed created the default tenant — but no user. A login page with no account behind it. You'd type credentials, get a correct "invalid credentials," and have no idea why. Fixed: a dev admin is now seeded (and refuses to seed in production, where the first admin is created via an explicit step).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The e2e suite caught a real one.&lt;/strong&gt; Once the end-to-end harness existed — real Postgres and Redis in throwaway containers, no mocks — it immediately caught an async tenant-context slip: a tenant-scoped query resolving &lt;em&gt;after&lt;/em&gt; its context had already exited. That's precisely the failure the whole multi-tenancy architecture exists to prevent, and it got caught before any buyer could ever meet it.&lt;/p&gt;

&lt;p&gt;That last one is the whole argument for writing the tests. The architecture was &lt;em&gt;designed&lt;/em&gt; to make cross-tenant leaks impossible, and the tests still found a path where it could slip. Design intent is not proof. Tests are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The receipts
&lt;/h2&gt;

&lt;p&gt;Here's what runs now, on every push, in CI. These are the actual suites — you get every one of these files in the zip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;span class="go"&gt;
 PASS  tenant.extension (structural isolation)   9
 PASS  auth/permissions.guard                     6
 PASS  security/webhooks (Stripe)                 5
 PASS  auth/token-duration                        3
 PASS  auth/password.service                      2

 Test Suites: 5 passed, 5 total
 Tests:       25 passed, 25 total
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm run &lt;span class="nb"&gt;test&lt;/span&gt;:e2e   &lt;span class="c"&gt;# real Postgres + Redis via Testcontainers&lt;/span&gt;
&lt;span class="go"&gt;
 PASS  App e2e
   ✓ logs in, hits a protected route, refreshes, hits it again
   ✓ hides tenant A data from a query run in tenant B context
   ✓ accepts a signed webhook once, de-duplicates the replay

 Tests: 3 passed, 3 total
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No mocks in the e2e path. Tenant isolation and webhook idempotency are proven against actual databases, not a stubbed client that always agrees with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I actually cared about getting right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Structural multi-tenancy — not a &lt;code&gt;where&lt;/code&gt; clause you have to remember.&lt;/strong&gt; A Prisma extension detects tenant-scoped models and forces &lt;code&gt;tenantId&lt;/code&gt; onto every read and write: filters merged onto finds, cross-tenant updates that simply can't match a row, and — critically — it's &lt;em&gt;fail-closed&lt;/em&gt;. A scoped model with no tenant in context throws, rather than silently returning everything. You add a model with a &lt;code&gt;tenantId&lt;/code&gt; column and it's isolated the moment you declare it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Stripe webhook that does the three hard things.&lt;/strong&gt; Signature verified against the &lt;em&gt;raw&lt;/em&gt; body (not the parsed one). Idempotent on the event ID in Redis, so a retried event never double-fulfills. Server-authoritative amounts — re-verified, never trusted from the payload. These are the three places sloppy kits quietly lose money, and they're the first thing I'd check before trusting anyone else's billing code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config that refuses to start wrong.&lt;/strong&gt; Every env var is validated at boot with Joi. Feature flags flip their own secrets from optional to required — you can't half-enable billing or email and discover it in production. If the config is wrong, the API doesn't start. Fail-closed beats fail-confusing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is NOT
&lt;/h2&gt;

&lt;p&gt;Being honest about this is the point, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's plumbing, not a finished product.&lt;/strong&gt; The frontend is an admin shell and a login. There are no marketing pages and no pre-built UI themes. Your product's screens are yours to build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing is wired and secured, not designed.&lt;/strong&gt; The webhook, idempotency, and security baseline are done. Your plans and checkout UI are not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's opinionated.&lt;/strong&gt; Decoupled API + SSR frontend, TypeScript everywhere, PostgreSQL, Node 24. If you want a one-folder monolith, this isn't it — and that's deliberate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you were hoping to buy a ready-to-sell app, this isn't that. If you were hoping to skip three weeks of auth-tenancy-billing groundwork and the security mistakes that come with rushing it, this is exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Nuxt 4 · Vue 3 · NestJS 11 · TypeScript · Prisma 6 · PostgreSQL 17 · Redis · BullMQ · Stripe · MinIO/S3 · Resend · Docker Compose · Nginx · Node 24 · GitHub Actions CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;p&gt;NuxtForge is a one-time purchase — €89 for a single developer, €199 for up to five seats / client work — sold through a merchant of record so VAT is handled at checkout. It's at &lt;strong&gt;&lt;a href="https://nuxtforge.com" rel="noopener noreferrer"&gt;nuxtforge.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But the reason I wrote this isn't the price. It's the process. If you're building or buying a starter kit, review it like a hostile buyer &lt;em&gt;before&lt;/em&gt; it matters — run its tests from a clean clone, boot it on a machine that isn't yours, and make it prove the things its README claims. A kit that can survive that is worth building on. One that can't will cost you the three weeks it promised to save, plus interest.&lt;/p&gt;

&lt;p&gt;Happy to answer anything about the multi-tenancy approach or the webhook handling in the comments.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>nuxt</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
