<?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: Alejandro Olivar</title>
    <description>The latest articles on DEV Community by Alejandro Olivar (@aomalejandrodev).</description>
    <link>https://dev.to/aomalejandrodev</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%2F4000724%2Fc13930a9-a8de-411a-8baf-c09c033a3d46.png</url>
      <title>DEV Community: Alejandro Olivar</title>
      <link>https://dev.to/aomalejandrodev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aomalejandrodev"/>
    <language>en</language>
    <item>
      <title>Deep Dive: Handling Multi-Tenancy Subdomains and DB Isolation without breaking Next.js 16</title>
      <dc:creator>Alejandro Olivar</dc:creator>
      <pubDate>Thu, 25 Jun 2026 16:07:04 +0000</pubDate>
      <link>https://dev.to/aomalejandrodev/deep-dive-handling-multi-tenancy-subdomains-and-db-isolation-without-breaking-nextjs-16-277b</link>
      <guid>https://dev.to/aomalejandrodev/deep-dive-handling-multi-tenancy-subdomains-and-db-isolation-without-breaking-nextjs-16-277b</guid>
      <description>&lt;h2&gt;
  
  
  The Boring Portfolio Problem
&lt;/h2&gt;

&lt;p&gt;Let's be honest. Most developer portfolios look exactly the same: a clean minimalist template, a grid of generic projects, and a bulleted list of tech stacks.&lt;/p&gt;

&lt;p&gt;When I built my portfolio, I wanted to showcase real-world production engineering. Instead of pushing 10 tiny code sandboxes, I decided to focus on deep-dive case studies.&lt;/p&gt;

&lt;p&gt;This is the breakdown of Äbasto, a full-stack, multi-tenant B2B SaaS for warehouse and POS management that handles data isolation, dynamic subdomains, and an automated grace-period subscription model under a single pnpm workspace monorepo.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏗️ The Architecture Stack
&lt;/h2&gt;

&lt;p&gt;he project is structured as a scalable monorepo using pnpm workspaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt;: Next.js 16 App Router, Zustand (state persistence), and Tailwind CSS v4.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt;: NestJS 11, TypeORM, and PostgreSQL.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🛡️ Challenge 1: Tenant Data Isolation at DB Level (PostgreSQL RLS)
&lt;/h2&gt;

&lt;p&gt;When building a B2B SaaS where multiple independent warehouses manage inventory, global multi-tenancy leaks are your worst nightmare. Adding &lt;code&gt;WHERE warehouse_id = X&lt;/code&gt; to every database query is prone to human error and scale bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution: Row-Level Security (RLS)&lt;/strong&gt;&lt;br&gt;
I delegated data isolation directly to PostgreSQL using Row-Level Security.&lt;/p&gt;

&lt;p&gt;Every database transaction runs securely isolated. A custom JwtAuthGuard in NestJS intercepts the request, decodes the tenant data, and injects session variables directly using SQL SET LOCAL commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A high-level view of injecting session context dynamically&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;injectTenantContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QueryRunner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;warehouseId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Safe runtime execution within the request transaction block&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SET LOCAL app.current_warehouse_id = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;warehouseId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;In the DB layer, tables enforce isolation natively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ALTER&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt; &lt;span class="nx"&gt;ENABLE&lt;/span&gt; &lt;span class="nx"&gt;ROW&lt;/span&gt; &lt;span class="nx"&gt;LEVEL&lt;/span&gt; &lt;span class="nx"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;CREATE&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt; &lt;span class="nx"&gt;warehouse_isolation_policy&lt;/span&gt; &lt;span class="nx"&gt;ON&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt;
    &lt;span class="nc"&gt;USING &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;warehouse_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.current_warehouse_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means even if a developer forgets to filter by warehouse in a frontend component, PostgreSQL will completely block cross-tenant data leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 Challenge 2: Dynamic Multi-Tenant Subdomains in Next.js 16
&lt;/h2&gt;

&lt;p&gt;wanted every warehouse owner to have their own distinct subdomain (e.g., &lt;code&gt;my-store.lvh.me:3000&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution: Dynamic Rewrite Proxy&lt;/strong&gt;&lt;br&gt;
Instead of cluttering the system with a heavy middleware.ts, I utilized a specialized server-side proxy.ts execution block in Next.js 16. It dynamically reads the Host header and rewrites paths directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSubdomainRewrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;)&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// e.g., 'bodega-x.lvh.me:3000'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="c1"&gt;// Bypass reserved internal system paths natively&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;subdomain&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;www&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Perform an internal server rewrite to the dynamic store template&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`/store/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subdomain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;&lt;strong&gt;The Catch: Server-Side Token Security&lt;/strong&gt;&lt;br&gt;
o prevent identity spoofing, the proxy reads the &lt;code&gt;token&lt;/code&gt; cookie (configured with a root domain scope &lt;code&gt;domain=.lvh.me&lt;/code&gt;), decodes the JWT payload on the server side, and natively verifies if the token's authorized warehouse matches the requested subdomain. If there is a mismatch, it triggers an immediate rewrite redirect to &lt;code&gt;/no-access&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⏳ Challenge 3: Automated Lock-Out &amp;amp; Subscription Engine
&lt;/h2&gt;

&lt;p&gt;A true SaaS needs to handle monetization and enforcement without blocking access to historical data arbitrarily. I designed a customized multi-state subscription model with an embedded 3-day grace period.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Active State -&amp;gt; [Expiration Date] -&amp;gt; 3-Day Grace Period (Banners) -&amp;gt; Fully Locked POS Screen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Backend Enforcement Guard&lt;/strong&gt;&lt;br&gt;
We built a centralized &lt;code&gt;SubscriptionGuard&lt;/code&gt; applied globally to all mutable endpoints (&lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;) across the products, inventory, and supplier components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;SubscriptionGuard&lt;/span&gt; &lt;span class="kr"&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="k"&gt;async&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;req&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gracePeriodDays&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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="c1"&gt;// Appended by auth verification&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;absoluteDeadline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gracePeriodDays&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;absoluteDeadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ForbiddenException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Subscription completely expired. Write operations locked.&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="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="c1"&gt;// GET endpoints remain open cleanly&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;&lt;strong&gt;The Frontend Reaction Flow&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Within 5 days of expiration:&lt;/strong&gt; A contextual Amber &lt;code&gt;SubscriptionBanner&lt;/code&gt; pops up in the Dashboard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;During Grace Period:&lt;/strong&gt; An Orange warning stays fixed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Past Grace Period:&lt;/strong&gt; A full-screen &lt;code&gt;SubscriptionLock&lt;/code&gt; overlay takes over the POS component with a pre-configured WhatsApp manual link (&lt;code&gt;wa.me&lt;/code&gt;) leveraging a centralized support module to handle instant payment updates.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📨 Challenge 4: Transactional Communications via Resend &amp;amp; Manual WhatsApp Fallbacks
&lt;/h2&gt;

&lt;p&gt;To ensure smooth operational communication without overhead costs, I implemented a hybrid Dual-Channel Notification System:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Channel A (Automated Transacational Mail):&lt;/strong&gt; A global &lt;code&gt;NotificationsModule&lt;/code&gt; hooks into backend services using the Resend SDK. It triggers beautifully designed, dark Neobrutalist HTML templates on critical events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;sendWelcomeEmail&lt;/code&gt;: Sends temporary credentials and system subdomain links immediately upon setup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;sendSubscriptionExpiringEmail&lt;/code&gt;: Triggered daily at noon via a NestJS @nestjs/schedule CRON job with clean in-memory de-duplication (Set).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Channel B (Manual WhatsApp Flows):&lt;/strong&gt; For payment tracking, the SuperAdmin dashboard incorporates manual communication helpers that parse tenant states dynamically into contextual text reminders, generating a frictionless single-click chat initiation link.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🧠 Key Takeaways
&lt;/h2&gt;

&lt;p&gt;Building &lt;strong&gt;Äbasto&lt;/strong&gt; proved that your portfolio doesn't need to be an archive of 20 unmaintained projects. Dedicating your space to full-scale engineering breakdowns showcases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Deep comprehension of DB performance and security models.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Familiarity with server-side network engineering architecture (proxies, domain parsing).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Product mindset implementation (subscription gates, user retention design).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What does your current portfolio project stack look like? Are you team Single-DB isolation or separated clusters? Let's discuss in the comments below!&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>nestjs</category>
      <category>postgres</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
