<?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>Forem: dan</title>
    <description>The latest articles on Forem by dan (@mr__complicated).</description>
    <link>https://forem.com/mr__complicated</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3863758%2F4bb7e22a-38ba-428f-8af7-15893b90adfd.gif</url>
      <title>Forem: dan</title>
      <link>https://forem.com/mr__complicated</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mr__complicated"/>
    <language>en</language>
    <item>
      <title>I Built a Multi-Tenant Car Dealer SaaS in 2 Weeks — Here's the Stack</title>
      <dc:creator>dan</dc:creator>
      <pubDate>Wed, 08 Apr 2026 06:31:00 +0000</pubDate>
      <link>https://forem.com/mr__complicated/i-built-a-multi-tenant-car-dealer-saas-in-2-weeks-heres-the-stack-d5c</link>
      <guid>https://forem.com/mr__complicated/i-built-a-multi-tenant-car-dealer-saas-in-2-weeks-heres-the-stack-d5c</guid>
      <description>&lt;p&gt;I Built a Multi-Tenant Car Dealer SaaS in 2 Weeks — Here's the Stack&lt;/p&gt;

&lt;p&gt;I wanted to solve a real problem: used car dealers need websites but can't afford developers. So I built ListKars — a platform where dealers sign up, pick a theme, and get a live website in minutes. Free.&lt;/p&gt;

&lt;p&gt;Here's how it works and what I learned.&lt;/p&gt;

&lt;p&gt;The Problem&lt;/p&gt;

&lt;p&gt;Most preowned car dealers run their business from WhatsApp and OLX. They don't have websites because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom development costs $500-$5,000+&lt;/li&gt;
&lt;li&gt;WordPress is too complex for non-tech people&lt;/li&gt;
&lt;li&gt;Marketplace listings (OLX, CarDekho) don't build their brand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to give them a branded website with zero setup cost.&lt;/p&gt;

&lt;p&gt;The Stack&lt;/p&gt;

&lt;p&gt;Monorepo:    Turborepo + pnpm workspaces&lt;br&gt;
  Backend:     NestJS + Fastify + SWC&lt;br&gt;
  Database:    PostgreSQL + Drizzle ORM&lt;br&gt;
  Frontend:    Next.js 15 (App Router) + Tailwind CSS 4&lt;br&gt;
  Storage:     MinIO (S3-compatible) + Sharp for image optimization&lt;br&gt;
  Search:      Meilisearch&lt;br&gt;
  Cache:       Redis&lt;br&gt;
  Auth:        Google OAuth + JWT + refresh tokens&lt;br&gt;
  Deployment:  Docker Compose on a VPS + Caddy reverse proxy&lt;br&gt;
  CDN:         Cloudflare&lt;/p&gt;

&lt;p&gt;Architecture: Multi-Tenancy&lt;/p&gt;

&lt;p&gt;Single database, tenant_id on every table. Each dealer gets dealer-name.listkars.com. Custom domains supported via a tenant_domains table.&lt;/p&gt;

&lt;p&gt;The storefront is one Next.js app that reads x-tenant-slug from the request header (set by Caddy based on subdomain) and renders the right dealer's data.&lt;/p&gt;

&lt;p&gt;Request: &lt;a href="https://automax-mumbai.listkars.com" rel="noopener noreferrer"&gt;https://automax-mumbai.listkars.com&lt;/a&gt;&lt;br&gt;
    → Caddy extracts subdomain → sets x-tenant-slug header&lt;br&gt;
    → Next.js middleware reads it → fetches tenant data&lt;br&gt;
    → Renders the dealer's theme with their cars&lt;/p&gt;

&lt;p&gt;7 Themes, One Codebase&lt;/p&gt;

&lt;p&gt;Each theme is a folder with layout.tsx, home-page.tsx, car-detail-page.tsx, and a CSS module. The main page.tsx has an if-else chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;themeName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme-alba&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AlbaLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AlbaLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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;themeName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme-carswitch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CarSwitchLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CarSwitchLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;// ... 5 more themes&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ClassicLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ClassicLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Themes range from a simple car grid (Classic) to a full landing page with hero, stats, and showroom section (Alba). Dealers switch themes from their dashboard — no code needed.&lt;/p&gt;

&lt;p&gt;Plugin System&lt;/p&gt;

&lt;p&gt;Not every dealer needs every feature. So I built a plugin system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plugins table: platform-wide available features&lt;/li&gt;
&lt;li&gt;tenant_plugins table: which dealer has what enabled&lt;/li&gt;
&lt;li&gt;PluginGate component on the storefront:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PluginGate&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"emi-calculator"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EmiCalculator&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;PluginGate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the dealer hasn't enabled the EMI Calculator plugin, it doesn't render. Currently 9 plugins: QR codes, PDF brochure, car compare, video listings, EMI calculator, testimonials, WhatsApp button, lead&lt;br&gt;
  notifications, visitor analytics.&lt;/p&gt;

&lt;p&gt;Lead Management&lt;/p&gt;

&lt;p&gt;This is where the business value is. Every inquiry from the storefront creates a lead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Status pipeline: New → Contacted → Interested → Won/Lost&lt;/li&gt;
&lt;li&gt;Follow-up reminders with snooze&lt;/li&gt;
&lt;li&gt;Duplicate detection (same phone + same car within 24h)&lt;/li&gt;
&lt;li&gt;Returning buyer tagging&lt;/li&gt;
&lt;li&gt;CSV export&lt;/li&gt;
&lt;li&gt;Response time tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The leads page has tabs for "All Leads" and "Follow-ups Due Today" — because dealers check this on their phone every morning.&lt;/p&gt;

&lt;p&gt;Currency &amp;amp; Locale from DB&lt;/p&gt;

&lt;p&gt;Each tenant has country and currency_code columns. A shared formatPrice() function uses toLocaleString() with the right locale:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;// ₹8,00,000 for Indian dealers&lt;br&gt;
  // $125,000 for US dealers&lt;br&gt;
  // AED 104,999 for UAE dealers&lt;br&gt;
  formatPrice(price, currencyCode, country)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;View Tracking That Doesn't Lie&lt;/p&gt;

&lt;p&gt;Car views are tracked but with safeguards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same visitor + same car within 30 min = skipped (no refresh inflation)&lt;/li&gt;
&lt;li&gt;Bot user-agents filtered (Googlebot, Bingbot, etc.)&lt;/li&gt;
&lt;li&gt;Dealer's own views skipped (referrer from admin dashboard)&lt;/li&gt;
&lt;li&gt;Unique visitors counted separately from total views&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I'd Do Differently&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Theme system needs abstraction. The page.tsx if-else chain is 700+ lines. Should be a registry pattern.&lt;/li&gt;
&lt;li&gt;Should have used tRPC instead of raw fetch calls between Next.js and NestJS.&lt;/li&gt;
&lt;li&gt;Drizzle ORM is great but the migration story with drizzle-kit in a monorepo is painful. Schema push works, generated migrations often conflict.&lt;/li&gt;
&lt;li&gt;ISR (60s revalidation) on the storefront was a game changer — one line of code, massive performance improvement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Numbers&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;7 themes&lt;/li&gt;
&lt;li&gt;9 plugins&lt;/li&gt;
&lt;li&gt;143 e2e tests&lt;/li&gt;
&lt;li&gt;~70 files changed per major feature&lt;/li&gt;
&lt;li&gt;Deployed on a single VPS with Docker Compose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try It&lt;/p&gt;

&lt;p&gt;&lt;a href="https://listkars.com" rel="noopener noreferrer"&gt;listkars.com&lt;/a&gt; — sign up, pick a theme, add a car. Your site is live in 2 minutes.&lt;/p&gt;

&lt;p&gt;The onboarding flow has a live preview that updates as you type your business name and pick a theme.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're building a multi-tenant SaaS, the biggest lesson: start with the tenant isolation pattern on day one. Retrofitting tenant_id later is painful.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>nestjs</category>
      <category>typescript</category>
      <category>sass</category>
    </item>
    <item>
      <title>How to Build Multi-Tenant Subdomains with Next.js 15 and Middleware</title>
      <dc:creator>dan</dc:creator>
      <pubDate>Mon, 06 Apr 2026 15:29:37 +0000</pubDate>
      <link>https://forem.com/mr__complicated/how-to-build-multi-tenant-subdomains-with-nextjs-15-and-middleware-j7p</link>
      <guid>https://forem.com/mr__complicated/how-to-build-multi-tenant-subdomains-with-nextjs-15-and-middleware-j7p</guid>
      <description>&lt;h2&gt;
  
  
  How to Build Multi-Tenant Subdomains with Next.js 15 and Middleware
&lt;/h2&gt;

&lt;p&gt;Building a SaaS where each customer gets their own subdomain (&lt;code&gt;customer.yourdomain.com&lt;/code&gt;)? Here's how I did it with Next.js 15 App Router.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tenant-a.listkars.com  →  Same Next.js app
tenant-b.listkars.com  →  Same Next.js app
tenant-c.listkars.com  →  Same Next.js app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One deployment, many tenants. The subdomain determines which data to show.&lt;/p&gt;

&lt;h2&gt;
  
  
  3 Step 1: Middleware to Extract Subdomain
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&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;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&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="nx"&gt;NextRequest&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;request&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="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="o"&gt;??&lt;/span&gt; &lt;span class="dl"&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;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;// Skip for main domain&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;www&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;listkars&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Pass subdomain to the app via header&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;response&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-tenant-slug&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&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: Read Tenant in Server Components
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/[[...slug]]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/headers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;Page&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;headersList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;headers&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;tenantSlug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headersList&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;x-tenant-slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Fetch tenant data from API&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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="nx"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/storefront/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantSlug&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="c1"&gt;// Render tenant's theme with their data&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: Database Design
&lt;/h3&gt;

&lt;p&gt;Single database with &lt;code&gt;tenant_id&lt;/code&gt; on every table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;cars&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;-- ... other fields&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_cars_tenant&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;cars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every query filters by &lt;code&gt;tenant_id&lt;/code&gt;. No data leakage between tenants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Reverse Proxy (Caddy)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*.listkars.com {
  reverse_proxy storefront:3001 {
    header_up X-Tenant-Slug {labels.3}
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy extracts the subdomain and passes it as a header.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Result
&lt;/h3&gt;

&lt;p&gt;Each dealer at &lt;a href="https://listkars.com" rel="noopener noreferrer"&gt;ListKars&lt;/a&gt; gets their own subdomain with a unique theme, car listings, and lead management — all from a single Next.js deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Middleware for routing&lt;/strong&gt; — extract subdomain, pass as header&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single DB, tenant_id everywhere&lt;/strong&gt; — simple and scales well&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One deployment&lt;/strong&gt; — no per-tenant infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ISR caching&lt;/strong&gt; — &lt;code&gt;revalidate = 60&lt;/code&gt; means pages are fast but data stays fresh&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;This pattern powers &lt;a href="https://listkars.com" rel="noopener noreferrer"&gt;listkars.com&lt;/a&gt; — a free platform for car dealers to create branded websites.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>sass</category>
      <category>tutorial</category>
      <category>nestjs</category>
    </item>
  </channel>
</rss>
