<?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: joah levien</title>
    <description>The latest articles on DEV Community by joah levien (@joah_a8278531aea1f).</description>
    <link>https://dev.to/joah_a8278531aea1f</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%2F2675749%2F5c89f671-ea64-4a25-a8fb-9f4c6e441078.jpeg</url>
      <title>DEV Community: joah levien</title>
      <link>https://dev.to/joah_a8278531aea1f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joah_a8278531aea1f"/>
    <language>en</language>
    <item>
      <title>How I Built Multi-Tenancy with Automatic Data Isolation in NestJS + TypeORM</title>
      <dc:creator>joah levien</dc:creator>
      <pubDate>Thu, 18 Jun 2026 14:52:24 +0000</pubDate>
      <link>https://dev.to/joah_a8278531aea1f/how-i-built-multi-tenancy-with-automatic-data-isolation-in-nestjs-typeorm-hp5</link>
      <guid>https://dev.to/joah_a8278531aea1f/how-i-built-multi-tenancy-with-automatic-data-isolation-in-nestjs-typeorm-hp5</guid>
      <description>&lt;p&gt;Multi-tenancy is the feature that separates a SaaS side project from a real SaaS product. But most tutorials teach it wrong — they rely on middleware filters or manual WHERE clauses that developers can forget.&lt;/p&gt;

&lt;p&gt;I built a system where &lt;strong&gt;tenant isolation happens at the repository level&lt;/strong&gt;. Developers cannot accidentally query another tenant's data. Here's how.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with "Manual" Multi-Tenancy
&lt;/h2&gt;

&lt;p&gt;Most multi-tenancy tutorials tell you to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract the tenant ID from the JWT&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;WHERE organizationId = ?&lt;/code&gt; clause to every query&lt;/li&gt;
&lt;li&gt;Hope nobody forgets&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a data leak waiting to happen. One missed filter, one raw query, one eager-loaded relation without the scope — and you're serving Customer A's data to Customer B.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Repository-Level Isolation
&lt;/h2&gt;

&lt;p&gt;Instead of filtering at the query level, I scope at the &lt;strong&gt;repository level&lt;/strong&gt;. Every repository automatically filters by the current tenant. Developers use the repository normally — the isolation is invisible and mandatory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: The Organization Entity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Organization&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;PrimaryGeneratedColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;suspended&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deleted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;OneToMany&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;OrganizationMember&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrganizationMember&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Tenant-Aware Base Entity
&lt;/h3&gt;

&lt;p&gt;Every entity that belongs to a tenant extends this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantAwareEntity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;ManyToOne&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;JoinColumn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;organizationId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Organization&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Scoped Repository
&lt;/h3&gt;

&lt;p&gt;The magic happens here. Instead of using TypeORM's default repository, every tenant-aware entity uses a scoped repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REQUEST&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;TenantAwareEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="na"&gt;dataSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DataSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Extract org from authenticated request&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Repository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entityClass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;FindManyOptions&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Always scoped&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FindOneOptions&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Always scoped&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DeepPartial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Auto-assign tenant&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only delete within the tenant scope&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: RBAC Integration
&lt;/h3&gt;

&lt;p&gt;Four roles with clear permission boundaries:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Can Manage Members&lt;/th&gt;
&lt;th&gt;Can Edit Settings&lt;/th&gt;
&lt;th&gt;Can Delete Org&lt;/th&gt;
&lt;th&gt;Can View Data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Owner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Admin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Member&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Viewer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RolesGuard&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;CanActivate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;reflector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Reflector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requiredRoles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reflector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAllAndOverride&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;ROLES_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHandler&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClass&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;requiredRoles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switchToHttp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getRequest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;requiredRoles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organizationRole&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage in controllers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Roles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OWNER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ADMIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;members/invite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;inviteMember&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InviteMemberDto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inviteMember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Pattern Matters
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No accidental data leaks&lt;/strong&gt; — the repository enforces scoping, not the developer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No performance overhead&lt;/strong&gt; — it's just a WHERE clause, same as manual filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean API&lt;/strong&gt; — controllers and services don't deal with tenant logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit-friendly&lt;/strong&gt; — every action is automatically scoped and logged&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  This Is Part of Cloudrix
&lt;/h2&gt;

&lt;p&gt;I built this multi-tenancy system as part of &lt;a href="https://demo.cloudrix.io" rel="noopener noreferrer"&gt;Cloudrix&lt;/a&gt; — a production-ready NestJS 11 + Angular 21 SaaS starter kit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1c67lcq7jg9wrso6i85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1c67lcq7jg9wrso6i85.png" alt="Cloudrix Features" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Along with multi-tenancy, it includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete auth (OAuth, magic links, 2FA, JWT rotation)&lt;/li&gt;
&lt;li&gt;Stripe payments (subscriptions, usage billing, webhooks)&lt;/li&gt;
&lt;li&gt;Admin dashboard with MRR/churn analytics&lt;/li&gt;
&lt;li&gt;7 email templates via Resend&lt;/li&gt;
&lt;li&gt;Docker + AWS Terraform deployment&lt;/li&gt;
&lt;li&gt;50+ API endpoints with Swagger docs&lt;/li&gt;
&lt;li&gt;Audit logging with 20+ action types&lt;/li&gt;
&lt;li&gt;GDPR compliance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Free MIT lite version on GitHub.&lt;/strong&gt; Paid: $149-$399, one-time purchase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://demo.cloudrix.io" rel="noopener noreferrer"&gt;demo.cloudrix.io&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;How do you handle multi-tenancy in your NestJS apps? I'd love to hear different approaches in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>database</category>
    </item>
    <item>
      <title>The Real Cost of Building SaaS from Scratch: A Developer's Breakdown</title>
      <dc:creator>joah levien</dc:creator>
      <pubDate>Thu, 18 Jun 2026 14:52:00 +0000</pubDate>
      <link>https://dev.to/joah_a8278531aea1f/the-real-cost-of-building-saas-from-scratch-a-developers-breakdown-59n2</link>
      <guid>https://dev.to/joah_a8278531aea1f/the-real-cost-of-building-saas-from-scratch-a-developers-breakdown-59n2</guid>
      <description>&lt;p&gt;I've built 4 SaaS products from scratch. Every single time, I spent the first 2-3 months on infrastructure before writing a single line of product code.&lt;/p&gt;

&lt;p&gt;Let me break down exactly where that time goes — and what it actually costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup Tax: What Nobody Tells First-Time Founders
&lt;/h2&gt;

&lt;p&gt;When you start a SaaS, you don't start building your product. You start building:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Payment processing&lt;/li&gt;
&lt;li&gt;Email system&lt;/li&gt;
&lt;li&gt;Admin dashboard&lt;/li&gt;
&lt;li&gt;Multi-tenancy&lt;/li&gt;
&lt;li&gt;Deployment pipeline&lt;/li&gt;
&lt;li&gt;Security hardening&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these are your product. But you can't ship without them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Hours Breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Time (Hours)&lt;/th&gt;
&lt;th&gt;What's Involved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authentication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;40-80&lt;/td&gt;
&lt;td&gt;Email/password, OAuth, magic links, 2FA, JWT refresh rotation, email verification, account lockout, password reset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stripe Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20-40&lt;/td&gt;
&lt;td&gt;Subscriptions, webhooks (the edge cases alone take weeks), customer portal, invoices, payment retry, usage billing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-Tenancy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30-50&lt;/td&gt;
&lt;td&gt;Org model, role system (RBAC), team invitations, data isolation, org switching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Admin Dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20-30&lt;/td&gt;
&lt;td&gt;User management, revenue stats, audit logs, search, pagination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Email System&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10-20&lt;/td&gt;
&lt;td&gt;Template design, transactional emails (welcome, reset, invitation, invoice), retry logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20-40&lt;/td&gt;
&lt;td&gt;Docker setup, CI/CD pipeline, infrastructure-as-code, SSL, domains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15-25&lt;/td&gt;
&lt;td&gt;Rate limiting, CORS, CSP headers, API key management, audit logging, GDPR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;155-285 hours&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At &lt;strong&gt;$50-$100/hour&lt;/strong&gt; (freelance or opportunity cost), that's &lt;strong&gt;$7,750-$28,500&lt;/strong&gt; in developer time.&lt;/p&gt;

&lt;p&gt;And that's an optimistic estimate. I'm not counting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debugging Stripe webhook edge cases at 2 AM&lt;/li&gt;
&lt;li&gt;The auth vulnerability you discover 3 months in&lt;/li&gt;
&lt;li&gt;Rewriting your tenant isolation after a data leak scare&lt;/li&gt;
&lt;li&gt;The deployment pipeline that works locally but breaks in production&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Math Speaks for Itself
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4y8jf4ji480oq1lhtqyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4y8jf4ji480oq1lhtqyq.png" alt="Cost comparison — 80-160 hours, $4K-$16K developer cost vs $249 one-time" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  "But I'll Learn More by Building It Myself"
&lt;/h2&gt;

&lt;p&gt;Yes. And you'll also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spend 3 months before your first user&lt;/li&gt;
&lt;li&gt;Burn through motivation on solved problems&lt;/li&gt;
&lt;li&gt;Ship auth with security holes you don't know about yet&lt;/li&gt;
&lt;li&gt;Build a worse version of what already exists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Learning is valuable. But if your goal is to &lt;strong&gt;ship a product&lt;/strong&gt;, every hour on boilerplate is an hour not spent on the thing that makes your SaaS unique.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But Boilerplates Are Hard to Customize"
&lt;/h2&gt;

&lt;p&gt;This is the one legitimate concern. Bad boilerplates are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Over-abstracted (10 layers to change a button)&lt;/li&gt;
&lt;li&gt;Poorly documented&lt;/li&gt;
&lt;li&gt;Abandoned after launch&lt;/li&gt;
&lt;li&gt;Built as npm packages you can't modify&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good boilerplates give you &lt;strong&gt;full source code&lt;/strong&gt; — you own it, you modify it, you delete what you don't need.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Good SaaS Starter Actually Saves You
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1c67lcq7jg9wrso6i85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1c67lcq7jg9wrso6i85.png" alt="Feature grid — what's included vs what you'd build from scratch" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I finally packaged my boilerplate into &lt;a href="https://demo.cloudrix.io" rel="noopener noreferrer"&gt;Cloudrix&lt;/a&gt;, I made sure it included everything I'd been rebuilding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth that's actually complete:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email/password + Google OAuth + magic links + 2FA (TOTP with backup codes)&lt;/li&gt;
&lt;li&gt;JWT with refresh token rotation&lt;/li&gt;
&lt;li&gt;Account lockout after failed attempts&lt;/li&gt;
&lt;li&gt;Email verification flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stripe that handles the edge cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subscriptions, one-time payments, usage-based billing&lt;/li&gt;
&lt;li&gt;Customer portal, webhook handling, invoice management&lt;/li&gt;
&lt;li&gt;Payment retry logic — the stuff that takes weeks to get right&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenancy that doesn't leak data:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4 RBAC roles (Owner, Admin, Member, Viewer)&lt;/li&gt;
&lt;li&gt;Automatic tenant isolation at the repository level&lt;/li&gt;
&lt;li&gt;Developers cannot accidentally query another tenant's data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Enterprise security you'd skip "for now" and never add:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limiting (3 tiers)&lt;/li&gt;
&lt;li&gt;HMAC request signing&lt;/li&gt;
&lt;li&gt;API key management (generate, rotate, revoke)&lt;/li&gt;
&lt;li&gt;Audit logging with 20+ action types&lt;/li&gt;
&lt;li&gt;GDPR data export + deletion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deployment you don't have to figure out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker Compose for local dev&lt;/li&gt;
&lt;li&gt;Terraform for AWS (ECS, RDS, ElastiCache, S3, CloudFront)&lt;/li&gt;
&lt;li&gt;GitHub Actions CI/CD with approval gates&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Cloudrix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source files&lt;/td&gt;
&lt;td&gt;130+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test files&lt;/td&gt;
&lt;td&gt;55+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API endpoints&lt;/td&gt;
&lt;td&gt;50+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM entities&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email templates&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RBAC roles&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit actions&lt;/td&gt;
&lt;td&gt;20+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bcgkqoshg9yh2wpfhiy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bcgkqoshg9yh2wpfhiy.png" alt="Full Swagger API documentation" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Market Gap
&lt;/h2&gt;

&lt;p&gt;The SaaS boilerplate market is dominated by Next.js: ShipFast ($169), Supastarter ($299), MakerKit ($249).&lt;/p&gt;

&lt;p&gt;Cloudrix is built on &lt;strong&gt;NestJS 11 + Angular 21&lt;/strong&gt; — the only production-grade option for teams that prefer enterprise-grade TypeScript architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9c0wiwxeo3zt6i26zri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9c0wiwxeo3zt6i26zri.png" alt="Comparison table — Cloudrix vs competitors" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free:&lt;/strong&gt; MIT-licensed lite version on GitHub&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starter:&lt;/strong&gt; $149 — auth, Stripe, email, admin dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro:&lt;/strong&gt; $249 — + multi-tenancy, Docker, BullMQ, webhooks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise:&lt;/strong&gt; $399 — + AWS Terraform, CI/CD, GDPR, Sentry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One-time purchase. Lifetime updates. 14-day refund guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://demo.cloudrix.io" rel="noopener noreferrer"&gt;demo.cloudrix.io&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're about to start a SaaS and you're budgeting 3 months for infrastructure — don't. Spend that time on the features your users actually want.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's the most time you've wasted rebuilding boilerplate? I'd love to hear your horror stories in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>startup</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>NestJS + Angular vs Next.js for SaaS: Why I Chose the Unpopular Stack in 2026</title>
      <dc:creator>joah levien</dc:creator>
      <pubDate>Thu, 18 Jun 2026 14:51:30 +0000</pubDate>
      <link>https://dev.to/joah_a8278531aea1f/nestjs-angular-vs-nextjs-for-saas-why-i-chose-the-unpopular-stack-in-2026-4ehh</link>
      <guid>https://dev.to/joah_a8278531aea1f/nestjs-angular-vs-nextjs-for-saas-why-i-chose-the-unpopular-stack-in-2026-4ehh</guid>
      <description>&lt;p&gt;Every SaaS boilerplate on the market is built on Next.js. ShipFast, Supastarter, MakerKit, SaasBold — all of them.&lt;/p&gt;

&lt;p&gt;I went the other way. I built &lt;strong&gt;Cloudrix&lt;/strong&gt; on &lt;strong&gt;NestJS 11 + Angular 21&lt;/strong&gt;. Here's why, and what I learned.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpvmcizpo3ec2e3t5bsnr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpvmcizpo3ec2e3t5bsnr.png" alt="Cloudrix — NestJS + Angular SaaS Starter Kit" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Next.js Monoculture Problem
&lt;/h2&gt;

&lt;p&gt;When I searched StarterIndex for SaaS boilerplates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;38+ NestJS&lt;/strong&gt; starter kits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9+ NestJS + Next.js&lt;/strong&gt; combinations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only 7 NestJS + Angular&lt;/strong&gt; combinations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The market decided Angular teams don't build SaaS. But they do — especially in the enterprise space where Angular dominates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why NestJS + Angular Makes Sense for B2B SaaS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Architectural Consistency
&lt;/h3&gt;

&lt;p&gt;NestJS and Angular share the same DNA: modules, dependency injection, decorators, TypeScript-first. Your backend and frontend speak the same architectural language.&lt;/p&gt;

&lt;p&gt;With Next.js + NestJS, you're mixing two fundamentally different paradigms — React's functional composition on the frontend with NestJS's OOP/DI on the backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Enterprise Teams Already Use Angular
&lt;/h3&gt;

&lt;p&gt;Angular has 35%+ market share in enterprise applications. If your team already builds with Angular, forcing them onto React for a SaaS product creates unnecessary friction.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Separation of Concerns
&lt;/h3&gt;

&lt;p&gt;Next.js blurs the line between frontend and backend with Server Components, Server Actions, and API routes. That's powerful for some use cases, but for a complex SaaS with multi-tenancy, RBAC, webhook systems, and job queues — you want a &lt;strong&gt;real backend&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NestJS gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proper module boundaries&lt;/li&gt;
&lt;li&gt;Dependency injection&lt;/li&gt;
&lt;li&gt;Guards, interceptors, pipes&lt;/li&gt;
&lt;li&gt;TypeORM integration at the module level&lt;/li&gt;
&lt;li&gt;BullMQ for async processing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Deployment Flexibility
&lt;/h3&gt;

&lt;p&gt;Next.js wants to be deployed on Vercel. It works elsewhere, but the experience degrades.&lt;/p&gt;

&lt;p&gt;NestJS + Angular? Deploy anywhere. Docker, AWS ECS, Railway, any VPS. No vendor lock-in. I include Terraform configs for AWS and Docker Compose for local dev — you own your infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Feature Gap Is Real
&lt;/h2&gt;

&lt;p&gt;I compared what competitors include vs what I built:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9c0wiwxeo3zt6i26zri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9c0wiwxeo3zt6i26zri.png" alt="Feature comparison across SaaS boilerplates" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Cloudrix (NestJS+Angular)&lt;/th&gt;
&lt;th&gt;Next.js Competitors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docker Compose&lt;/td&gt;
&lt;td&gt;Full-stack included&lt;/td&gt;
&lt;td&gt;Almost none include it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Terraform&lt;/td&gt;
&lt;td&gt;ECS, RDS, S3, CloudFront&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-tenancy&lt;/td&gt;
&lt;td&gt;Automatic DB-level isolation&lt;/td&gt;
&lt;td&gt;Some have it, most don't&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BullMQ Job Queues&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit Logging&lt;/td&gt;
&lt;td&gt;20+ action types&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD Pipelines&lt;/td&gt;
&lt;td&gt;GitHub Actions with approval gates&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR Compliance&lt;/td&gt;
&lt;td&gt;Data export + deletion&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API Key Management&lt;/td&gt;
&lt;td&gt;Generate, rotate, revoke&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Next.js starters optimize for &lt;strong&gt;speed to first deploy&lt;/strong&gt;. Cloudrix optimizes for &lt;strong&gt;production readiness&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tradeoffs (I'll Be Honest)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where Next.js wins:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster initial page loads with SSR/SSG out of the box&lt;/li&gt;
&lt;li&gt;Larger ecosystem of UI component libraries&lt;/li&gt;
&lt;li&gt;More tutorials and community content&lt;/li&gt;
&lt;li&gt;Easier for solo developers who want full-stack in one framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where NestJS + Angular wins:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better for teams (clear frontend/backend separation)&lt;/li&gt;
&lt;li&gt;Stronger typing and module architecture&lt;/li&gt;
&lt;li&gt;More natural fit for complex business logic&lt;/li&gt;
&lt;li&gt;No vendor lock-in on deployment&lt;/li&gt;
&lt;li&gt;Enterprise teams don't need to retrain&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cloudrix&lt;/strong&gt; includes everything a SaaS needs on day one:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1c67lcq7jg9wrso6i85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1c67lcq7jg9wrso6i85.png" alt="Features grid — Auth, Payments, Database, Email, Admin, Docker" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auth:&lt;/strong&gt; Email/password, Google OAuth, magic links, 2FA, JWT rotation, account lockout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; Stripe subscriptions, usage-based billing, customer portal, webhooks, invoices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenancy:&lt;/strong&gt; 4 RBAC roles, org switching, automatic tenant isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin Dashboard:&lt;/strong&gt; User management, MRR/churn stats, audit logs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email:&lt;/strong&gt; 7 production templates via Resend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment:&lt;/strong&gt; Docker Compose, AWS Terraform, GitHub Actions CI/CD&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; Rate limiting, HMAC signing, API keys, CSP, audit logging, GDPR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;130+ source files. 55+ tests. 50+ API endpoints.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bcgkqoshg9yh2wpfhiy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bcgkqoshg9yh2wpfhiy.png" alt="Swagger API Documentation" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Free MIT-licensed lite version on GitHub. Paid tiers: $149 / $249 / $399 — one-time purchase, lifetime updates, 14-day refund guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://demo.cloudrix.io" rel="noopener noreferrer"&gt;demo.cloudrix.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've been looking for a production-ready NestJS + Angular SaaS starter — or you're just tired of the Next.js monoculture — I built this for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your experience building SaaS with Angular? Is the lack of boilerplates something that's held you back? I'd love to hear in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>angular</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Production-Ready NestJS + Angular SaaS Starter Kit — Here's Everything Inside</title>
      <dc:creator>joah levien</dc:creator>
      <pubDate>Thu, 18 Jun 2026 14:31:27 +0000</pubDate>
      <link>https://dev.to/joah_a8278531aea1f/i-built-a-production-ready-nestjs-angular-saas-starter-kit-heres-everything-inside-3221</link>
      <guid>https://dev.to/joah_a8278531aea1f/i-built-a-production-ready-nestjs-angular-saas-starter-kit-heres-everything-inside-3221</guid>
      <description>&lt;p&gt;Every SaaS founder builds the same things from scratch: auth, payments, multi-tenancy, admin dashboard, email, deployment. That's &lt;strong&gt;80-160 hours&lt;/strong&gt; and &lt;strong&gt;$4K-$16K&lt;/strong&gt; in developer time — before writing a single line of business logic.&lt;/p&gt;

&lt;p&gt;I got tired of rebuilding the same boilerplate for the 4th time. So I packaged it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Cloudrix — SaaS Starter Kit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NestJS 11 + Angular 21&lt;/strong&gt; in an Nx monorepo. Production-ready. Buy once, get the full source code, build your SaaS on top.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We don't win by writing another login screen." — Every founder, eventually.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpvmcizpo3ec2e3t5bsnr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpvmcizpo3ec2e3t5bsnr.png" alt="Cloudrix Landing Page — Ship Your SaaS in Days, Not Months" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://demo.cloudrix.io" rel="noopener noreferrer"&gt;demo.cloudrix.io&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;NestJS 11, TypeScript 5.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Angular 21, Tailwind CSS v4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL + TypeORM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue&lt;/td&gt;
&lt;td&gt;Redis + BullMQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Resend (7 templates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infra&lt;/td&gt;
&lt;td&gt;Docker, Terraform (AWS), GitHub Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monorepo&lt;/td&gt;
&lt;td&gt;Nx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring&lt;/td&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What You Get — Feature by Feature
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1c67lcq7jg9wrso6i85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1c67lcq7jg9wrso6i85.png" alt="Cloudrix Features — Auth, Payments, Database, Email, Admin, Docker" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication (Complete, Not a Tutorial)
&lt;/h3&gt;

&lt;p&gt;Email/password, Google OAuth, magic links, &lt;strong&gt;two-factor authentication&lt;/strong&gt; (TOTP with QR codes + backup codes), JWT with refresh token rotation, account lockout after failed attempts, email verification.&lt;/p&gt;

&lt;p&gt;Multiple strategies: JWT, Local, Google — with role-based guards and decorators baked in.&lt;/p&gt;

&lt;p&gt;This isn't a blog post auth system. This is what you'd build after 3 years of production incidents taught you all the edge cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stripe Payments — Ready to Charge Day One
&lt;/h3&gt;

&lt;p&gt;Subscriptions, one-time payments, usage-based billing, customer portal, webhook handling, invoice management, payment retry logic.&lt;/p&gt;

&lt;p&gt;Stripe webhooks are a special kind of pain — dozens of edge cases around failed charges, subscription upgrades, proration, and dunning. All handled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Tenancy That Actually Isolates Data
&lt;/h3&gt;

&lt;p&gt;Organizations with &lt;strong&gt;4 roles&lt;/strong&gt; (Owner, Admin, Member, Viewer). Team invitations. Org switching.&lt;/p&gt;

&lt;p&gt;Shared database with &lt;strong&gt;automatic tenant isolation at the repository level&lt;/strong&gt;. Developers physically cannot query another tenant's data. Not "be careful" — &lt;em&gt;cannot&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Admin Dashboard
&lt;/h3&gt;

&lt;p&gt;User management (view, edit, suspend, delete), platform statistics (&lt;strong&gt;MRR, churn, signups&lt;/strong&gt;), audit logs for every admin action, role management, search, pagination. Dark mode included.&lt;/p&gt;

&lt;h3&gt;
  
  
  Email System — 7 Production Templates
&lt;/h3&gt;

&lt;p&gt;Via Resend with retry logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Welcome&lt;/li&gt;
&lt;li&gt;Email verification&lt;/li&gt;
&lt;li&gt;Password reset&lt;/li&gt;
&lt;li&gt;Magic link&lt;/li&gt;
&lt;li&gt;Team invitation&lt;/li&gt;
&lt;li&gt;Invoice&lt;/li&gt;
&lt;li&gt;Subscription confirmation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All styled. All tested. Ready to send.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment — Local to Production
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose&lt;/strong&gt; for local dev (full stack in one command)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Dockerfiles&lt;/strong&gt; with nginx config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; configs for AWS: ECS, RDS, ElastiCache, S3, CloudFront&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; CI/CD with approval gates&lt;/li&gt;
&lt;li&gt;Railway and Vercel configs included&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Security — Enterprise-Grade
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Rate limiting (3 tiers)&lt;/li&gt;
&lt;li&gt;HMAC request signing&lt;/li&gt;
&lt;li&gt;API key management (generate, rotate, revoke)&lt;/li&gt;
&lt;li&gt;CORS locked to frontend domain&lt;/li&gt;
&lt;li&gt;CSP headers + Helmet&lt;/li&gt;
&lt;li&gt;Audit logging — &lt;strong&gt;20+ action types&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;GDPR compliance (data export + deletion)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  50+ Production API Endpoints
&lt;/h2&gt;

&lt;p&gt;Full Swagger/OpenAPI documentation included. Every endpoint is documented, tested, and ready.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bcgkqoshg9yh2wpfhiy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bcgkqoshg9yh2wpfhiy.png" alt="Cloudrix Swagger API Documentation" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  By the Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source files&lt;/td&gt;
&lt;td&gt;130+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test files&lt;/td&gt;
&lt;td&gt;55+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API endpoints&lt;/td&gt;
&lt;td&gt;50+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM entities&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email templates&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RBAC roles&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit log actions&lt;/td&gt;
&lt;td&gt;20+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Why NestJS + Angular? Because the Market Has a Next.js Monoculture.
&lt;/h2&gt;

&lt;p&gt;I counted: &lt;strong&gt;38+ NestJS boilerplates&lt;/strong&gt; exist, but only &lt;strong&gt;7 are NestJS + Angular&lt;/strong&gt;. Meanwhile, ShipFast, Supastarter, MakerKit, and every trending boilerplate on Hacker News is Next.js-based.&lt;/p&gt;

&lt;p&gt;If your team already works with Angular and NestJS — or you prefer a strongly-typed, modular, enterprise-grade architecture — the market had almost nothing for you.&lt;/p&gt;

&lt;p&gt;Cloudrix fills that gap.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9c0wiwxeo3zt6i26zri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9c0wiwxeo3zt6i26zri.png" alt="Cloudrix Feature Comparison Table" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Math Speaks for Itself
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4y8jf4ji480oq1lhtqyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4y8jf4ji480oq1lhtqyq.png" alt="Cost comparison — 80-160 hours, $4K-$16K developer cost vs $249 Cloudrix" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Buyers Are Saying
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Saved us 3 weeks of boilerplate. The auth and Stripe integration are production-quality — we just added our business logic on top."&lt;/em&gt; — &lt;strong&gt;Alex Chen, CTO&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Finally a NestJS boilerplate that includes Docker and AWS. The Terraform modules alone are worth the Enterprise price."&lt;/em&gt; — &lt;strong&gt;Sarah Johnson, Senior Developer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"I used to spend the first month of every client project on auth and payments. Now I start with the actual features from day one."&lt;/em&gt; — &lt;strong&gt;Marcus Kim, Freelance Developer&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Pricing — One-Time Purchase, Not a Subscription
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Highlights&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;MIT-licensed, basic auth, Docker setup, project structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Starter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$149&lt;/td&gt;
&lt;td&gt;Full auth, Stripe, PostgreSQL, email, admin dashboard, Swagger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$249&lt;/td&gt;
&lt;td&gt;+ Docker full-stack, BullMQ, multi-tenancy, RBAC, S3, webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$399&lt;/td&gt;
&lt;td&gt;+ AWS Terraform, CI/CD, Nx microservices, k6 load testing, Sentry, GDPR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All paid plans: &lt;strong&gt;lifetime access, free updates, 14-day refund guarantee&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Free lite version&lt;/strong&gt; on GitHub — MIT-licensed with basic auth, Docker setup, and 380+ lines of documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://demo.cloudrix.io" rel="noopener noreferrer"&gt;demo.cloudrix.io&lt;/a&gt; — 55 pages including full Swagger API docs.&lt;/p&gt;

&lt;p&gt;You're not building a login system. You're building &lt;em&gt;your product&lt;/em&gt;. Start there.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://demo.cloudrix.io" rel="noopener noreferrer"&gt;demo.cloudrix.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>angular</category>
      <category>saas</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How we built multi-tenant isolation in NestJS that even a junior dev can't break</title>
      <dc:creator>joah levien</dc:creator>
      <pubDate>Thu, 18 Jun 2026 13:11:59 +0000</pubDate>
      <link>https://dev.to/joah_a8278531aea1f/how-we-built-multi-tenant-isolation-in-nestjs-that-even-a-junior-dev-cant-break-58h5</link>
      <guid>https://dev.to/joah_a8278531aea1f/how-we-built-multi-tenant-isolation-in-nestjs-that-even-a-junior-dev-cant-break-58h5</guid>
      <description>&lt;p&gt;About a year ago, a junior dev on our team wrote a cleanup job that nuked records without a tenant filter. In staging,&lt;br&gt;
   thankfully — but it wiped out an entire test tenant's data and took half a day to restore. That was the wake-up call.&lt;/p&gt;

&lt;p&gt;We run a multi-tenant NestJS + TypeORM SaaS (shared database, shared schema, &lt;code&gt;tenant_id&lt;/code&gt; column on everything). The&lt;br&gt;
  classic approach is "just remember to add &lt;code&gt;WHERE tenant_id = ?&lt;/code&gt; everywhere." Which works great right up until it&lt;br&gt;&lt;br&gt;
  doesn't.&lt;/p&gt;

&lt;p&gt;So we built a three-layer safety net. Sharing it because I haven't seen this exact combo written up anywhere, and it&lt;br&gt;
  took us a few iterations to get right.                                                                                &lt;/p&gt;

&lt;p&gt;## Layer 1: Every entity inherits tenant ownership                                                                    &lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
typescript                                                                                                         
  @Entity()                                                                                                             
  export abstract class TenantBaseEntity {
    @Column()                                                                                                           
    tenantId: string;                                                                                                 
  }                                       

  Dead simple. If an entity doesn't extend this, it doesn't get created. We enforce this in code review — no exceptions.
   It means the column physically exists on every table, which matters for Layer 3.

  Layer 2: Tenant context lives on the request                                                                          

  @Injectable({ scope: Scope.REQUEST })                                                                                 
  export class TenantService {                                                                                        
    private tenantId: string;             

    constructor(@Inject(REQUEST) private request: Request) {                                                            
      this.tenantId = this.request.user?.tenantId;
    }                                                                                                                   

    getTenantId(): string {
      if (!this.tenantId) {                                                                                             
        throw new Error('Tenant context not available — are you in a non-HTTP context?');                             
      }                                       
      return this.tenantId;               
    }                                                                                                                   
  }                                                                                                                     

  The key addition we made after getting burned: that guard clause. If something tries to query without a tenant        
  context, it throws loudly instead of silently returning unscoped data. Fail closed, not open.                       

  Layer 3: A custom repository that makes forgetting impossible                                                         

  @Injectable()                                                                                                         
  export class TenantRepository&amp;lt;T extends TenantBaseEntity&amp;gt; {                                                           
    constructor(
      private repo: Repository&amp;lt;T&amp;gt;,                                                                                      
      private tenantService: TenantService,                                                                           
    ) {}                                      

    async find(options?: FindManyOptions&amp;lt;T&amp;gt;): Promise&amp;lt;T[]&amp;gt; {
      return this.repo.find({                                                                                           
        ...options,                           
        where: {                                                                                                        
          ...options?.where,                                                                                          
          tenantId: this.tenantService.getTenantId(),                                                                   
        } as any,
      });                                                                                                               
    }                                                                                                                 

    async findOne(options?: FindOneOptions&amp;lt;T&amp;gt;): Promise&amp;lt;T | null&amp;gt; {
      return this.repo.findOne({
        ...options,                                                                                                     
        where: {
          ...options?.where,                                                                                            
          tenantId: this.tenantService.getTenantId(),                                                                 
        } as any,                             
      });                                 
    }

    // same pattern for save, update, delete...
  }                                                                                                                     

  Devs inject TenantRepository&amp;lt;Whatever&amp;gt; instead of the raw TypeORM repo. The tenant filter is injected automatically on
   every operation. You can't forget it because you never write it.

  The edge case that bit us: background jobs                                                                            

  Cron tasks, BullMQ workers — anything outside an HTTP request has no request-scoped context, so TenantService blows   
  up. We solved this with an explicit TenantContext wrapper:                                                            

  await this.tenantContext.runWithTenant(tenantId, async () =&amp;gt; {                                                        
    await this.tenantRepository.find();                                                                               
  });                                                                                                                   

  Honest tradeoffs

  Not gonna pretend this is perfect:          

  - Query performance — composite indexes on every table. Our DBA was not thrilled.                                   
  - Request-scoped injection — NestJS creates new instances per request. At scale, look into AsyncLocalStorage with     
  nestjs-cls.                                 
  - Raw queries — if someone writes raw SQL, none of this helps. We lint for query() and createQueryBuilder() in CI.    

  Running in production for about a year across ~40 tables. Zero cross-tenant incidents since.                          

  What's next?                                                                                                          

  Genuinely curious — anyone gone the schema-per-tenant route with NestJS? We evaluated it early but connection pool    
  management seemed nightmarish at ~200 tenants. Also wondering about Postgres RLS as an alternative.                   

  ---                                                                                                                   
  We packaged this pattern (along with auth, Stripe payments, RBAC, admin dashboard, and deployment configs) into a full
   SaaS starter kit:                                                                                                    

  - 🔗 https://github.com/sayahweb2-png/saas-starter-lite (MIT licensed)
  - 🔗 https://demo.cloudrix.io                                                                                         
  - 🔗 https://demo.cloudrix.io/blog/nestjs-angular-authentication-jwt-oauth

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>typescript</category>
      <category>saas</category>
      <category>nestjs</category>
    </item>
  </channel>
</rss>
