<?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: Abdullah Bayomy</title>
    <description>The latest articles on DEV Community by Abdullah Bayomy (@abdullah_bayomy_84cc12220).</description>
    <link>https://dev.to/abdullah_bayomy_84cc12220</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%2F3958381%2Fe5c9139b-1d10-4180-8686-745d447803f8.jpg</url>
      <title>DEV Community: Abdullah Bayomy</title>
      <link>https://dev.to/abdullah_bayomy_84cc12220</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abdullah_bayomy_84cc12220"/>
    <language>en</language>
    <item>
      <title>Building a Multi-Tenant Clinic SaaS with NestJS + Prisma: What 12 Months in Production Taught Me</title>
      <dc:creator>Abdullah Bayomy</dc:creator>
      <pubDate>Fri, 29 May 2026 11:48:48 +0000</pubDate>
      <link>https://dev.to/abdullah_bayomy_84cc12220/building-a-multi-tenant-clinic-saas-with-nestjs-prisma-what-12-months-in-production-taught-me-25mh</link>
      <guid>https://dev.to/abdullah_bayomy_84cc12220/building-a-multi-tenant-clinic-saas-with-nestjs-prisma-what-12-months-in-production-taught-me-25mh</guid>
      <description>&lt;h2&gt;
  
  
  The problem multi-tenant healthcare SaaS has to solve
&lt;/h2&gt;

&lt;p&gt;A clinic management product looks simple from the outside: appointments, patients, prescriptions, billing. The hard part isn't features — it's the &lt;strong&gt;tenancy model&lt;/strong&gt;.&lt;br&gt;
Real clinics broke my early assumptions fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One &lt;strong&gt;doctor&lt;/strong&gt; often owns &lt;strong&gt;multiple clinics&lt;/strong&gt; (a downtown branch + a hospital affiliation).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;receptionist&lt;/strong&gt; can work at &lt;strong&gt;clinic A in the morning&lt;/strong&gt; and &lt;strong&gt;clinic B in the evening&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;patient&lt;/strong&gt; may have visits at &lt;strong&gt;multiple clinics&lt;/strong&gt; of the same doctor — same medical history.&lt;/li&gt;
&lt;li&gt;Each &lt;strong&gt;clinic has its own subscription lifecycle&lt;/strong&gt; — active, trial, expired, suspended.&lt;/li&gt;
&lt;li&gt;Walk-ins outnumber scheduled appointments in this market, so the &lt;strong&gt;queue is the product&lt;/strong&gt;, not the calendar.
A naive "one tenant = one customer" model breaks on day one.
This post walks through the architecture I shipped for &lt;a href="https://tabeeb-hub.com" rel="noopener noreferrer"&gt;TabeebHub&lt;/a&gt;: a NestJS + Prisma + Postgres stack with a shared database, row-level tenant scoping, role-based access, and clinic-scoped subscriptions.
---
## The stack (and why)
| Layer | Choice | Why |
|-------|--------|-----|
| API | &lt;strong&gt;NestJS&lt;/strong&gt; | Modules + decorators map cleanly to clinics, staff, visits, billing |
| ORM | &lt;strong&gt;Prisma&lt;/strong&gt; | Type-safe queries; middleware hook is perfect for tenant scoping |
| DB | &lt;strong&gt;Postgres&lt;/strong&gt; | Mature row-level security, JSONB for prescription payloads, strong constraints |
| Auth | &lt;strong&gt;JWT&lt;/strong&gt; | Doctor + staff sessions, embedded role + clinic claims |
| Frontend | &lt;strong&gt;Next.js 14 (App Router)&lt;/strong&gt; | RSC, i18n routes for Arabic/English |
| Queue | &lt;strong&gt;BullMQ + Redis&lt;/strong&gt; | Appointment reminders, WhatsApp OTP, subscription expiry jobs |
| Payments | &lt;strong&gt;Stripe + local PSPs&lt;/strong&gt; | Stripe globally; local wallets (Fawry, Vodafone Cash) in Egypt |
Nothing exotic. The architecture wins or loses on &lt;strong&gt;how the boundaries between tenants are enforced&lt;/strong&gt;, not on the framework.
---
## Tenancy model: shared DB, scoped rows
Three real options I considered:&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB per tenant&lt;/strong&gt; — strongest isolation, operational nightmare at 100+ clinics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema per tenant&lt;/strong&gt; — better, but Prisma + migrations get awkward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared DB, tenant column on every row&lt;/strong&gt; — most operationally sane for B2B SaaS at our scale.
I picked option 3 with a strict rule: &lt;strong&gt;every tenant-owned table has a &lt;code&gt;clinicId&lt;/code&gt; foreign key, and every query is scoped by it at the ORM layer — not at the controller&lt;/strong&gt;.
Simplified schema:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Doctor {
  id            String    @id @default(cuid())
  email         String    @unique
  phone         String    @unique
  clinics       Clinic[]
  createdAt     DateTime  @default(now())
}
model Clinic {
  id            String          @id @default(cuid())
  ownerId       String
  owner         Doctor          @relation(fields: [ownerId], references: [id])
  name          String
  isActive      Boolean         @default(true)
  subscription  Subscription?
  staff         ClinicStaff[]
  patients      Patient[]
  visits        Visit[]
  createdAt     DateTime        @default(now())
  @@index([ownerId])
}
model ClinicStaff {
  id          String   @id @default(cuid())
  clinicId    String
  clinic      Clinic   @relation(fields: [clinicId], references: [id])
  userId      String
  role        StaffRole
  isActive    Boolean  @default(true)
  @@unique([clinicId, userId])
}
model Visit {
  id           String       @id @default(cuid())
  clinicId     String
  clinic       Clinic       @relation(fields: [clinicId], references: [id])
  patientId    String
  doctorId     String
  status       VisitStatus  // WAITING | WITH_ASSISTANT | WITH_DOCTOR | DONE
  startedAt    DateTime?
  finishedAt   DateTime?
  createdAt    DateTime     @default(now())
  @@index([clinicId, status])
}
enum VisitStatus {
  WAITING
  WITH_ASSISTANT
  WITH_DOCTOR
  DONE
  CANCELED
  NO_SHOW
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A few decisions worth pointing out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;clinicId&lt;/code&gt; is everywhere&lt;/strong&gt;. Visits, prescriptions, invoices, audit logs. No tenant-owned row exists without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;isActive&lt;/code&gt; on &lt;code&gt;Clinic&lt;/code&gt; is enforced at the service layer&lt;/strong&gt;. Deactivated clinics can't accept new visits, even via direct API calls.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  - &lt;strong&gt;Subscription lives on the clinic, not the doctor.&lt;/strong&gt; A doctor with 3 clinics can have 3 different plans.
&lt;/h2&gt;
&lt;h2&gt;
  
  
  Tenant scoping: Prisma middleware, not controller params
&lt;/h2&gt;

&lt;p&gt;The single biggest source of bugs in multi-tenant systems is &lt;strong&gt;forgetting to filter by tenant&lt;/strong&gt;. Junior devs do it. So do tired senior ones.&lt;br&gt;
I refuse to rely on every developer remembering. Instead, the request-scoped tenant context flows through Prisma middleware:&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="c1"&gt;// prisma/tenant.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;Prisma&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;@prisma/client&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AsyncLocalStorage&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;async_hooks&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;const&lt;/span&gt; &lt;span class="nx"&gt;tenantContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AsyncLocalStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;clinicId&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="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;TENANT_SCOPED_MODELS&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;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;Visit&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;Patient&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;Prescription&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;Invoice&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;AuditLog&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;ClinicStaff&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantMiddleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Middleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tenantContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStore&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;ctx&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;clinicId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;params&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;TENANT_SCOPED_MODELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&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="k"&gt;return&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;params&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;clinicId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createMany&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clinicId&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&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;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clinicId&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;findUnique&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;findFirst&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;findMany&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updateMany&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deleteMany&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;count&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;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;where&lt;/span&gt; &lt;span class="o"&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;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;where&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt; &lt;span class="nx"&gt;clinicId&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="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&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;A NestJS interceptor sets the tenant context per request:&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="c1"&gt;// common/interceptors/tenant.interceptor.ts&lt;/span&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;TenantInterceptor&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;NestInterceptor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&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;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CallHandler&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;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&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="nx"&gt;clinicId&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="nx"&gt;activeClinicId&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;clinicId&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;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&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;new&lt;/span&gt; &lt;span class="nc"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sub&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;tenantContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;clinicId&lt;/span&gt; &lt;span class="p"&gt;},&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;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&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="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;What this buys me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new developer writing &lt;code&gt;prisma.visit.findMany()&lt;/code&gt; literally cannot leak another clinic's visits.&lt;/li&gt;
&lt;li&gt;Cross-tenant queries (admin dashboards, internal reports) explicitly &lt;strong&gt;opt out&lt;/strong&gt; by skipping the interceptor — visible in code review.&lt;/li&gt;
&lt;li&gt;All tenant scoping logic lives in one file. Tests are trivial.
What this does &lt;strong&gt;not&lt;/strong&gt; buy me: protection from the doctor switching &lt;code&gt;activeClinicId&lt;/code&gt; in their JWT to a clinic they don't own. That belongs to the next layer.
---
## RBAC: visit status is the source of truth
Healthcare RBAC is more than "is this user a doctor or a receptionist." It depends on &lt;strong&gt;the state of the visit&lt;/strong&gt;.
Examples:&lt;/li&gt;
&lt;li&gt;A receptionist can edit patient info &lt;strong&gt;before&lt;/strong&gt; the visit starts, not after.&lt;/li&gt;
&lt;li&gt;A doctor can create a draft prescription &lt;strong&gt;only while&lt;/strong&gt; the visit is &lt;code&gt;WITH_DOCTOR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Nobody can modify a finalized prescription — only append addenda.
We compute a permission matrix from &lt;code&gt;(userRole, visitStatus)&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="o"&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;EDIT_PATIENT&lt;/span&gt;&lt;span class="dl"&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;CHECK_IN&lt;/span&gt;&lt;span class="dl"&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;START_VISIT&lt;/span&gt;&lt;span class="dl"&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;CREATE_DRAFT_PRESCRIPTION&lt;/span&gt;&lt;span class="dl"&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;FINALIZE_PRESCRIPTION&lt;/span&gt;&lt;span class="dl"&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;EDIT_FINAL_PRESCRIPTION&lt;/span&gt;&lt;span class="dl"&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;ISSUE_INVOICE&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;matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StaffRole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;VisitStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;DOCTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;WAITING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;WITH_ASSISTANT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;WITH_DOCTOR&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;CREATE_DRAFT_PRESCRIPTION&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;FINALIZE_PRESCRIPTION&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;DONE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;CANCELED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;NO_SHOW&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="na"&gt;RECEPTION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;WAITING&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;EDIT_PATIENT&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;CHECK_IN&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;START_VISIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;WITH_ASSISTANT&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;EDIT_PATIENT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;WITH_DOCTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;DONE&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;ISSUE_INVOICE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;CANCELED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;NO_SHOW&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="na"&gt;ADMIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* full */&lt;/span&gt; &lt;span class="p"&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;const&lt;/span&gt; &lt;span class="nx"&gt;can&lt;/span&gt; &lt;span class="o"&gt;=&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;StaffRole&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="nx"&gt;VisitStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&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="nx"&gt;matrix&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;status&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend uses the same matrix (shared TS package) to &lt;strong&gt;hide&lt;/strong&gt; the action; the backend uses it to &lt;strong&gt;enforce&lt;/strong&gt; the action. Single source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Mistake I made:&lt;/strong&gt; I first wired RBAC from &lt;code&gt;role&lt;/code&gt; only. Within two weeks, a receptionist accidentally edited a finalized prescription. Visit status had to become part of the permission key.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Subscriptions per clinic, not per account
&lt;/h2&gt;

&lt;p&gt;Most B2B SaaS bills by account. Clinic SaaS shouldn't.&lt;br&gt;
Why: a doctor with two clinics often runs them as separate businesses (different staff, different P&amp;amp;L). Forcing a single subscription means upselling fails and downgrades are painful.&lt;br&gt;
So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Subscription {
  id          String    @id @default(cuid())
  clinicId    String    @unique
  clinic      Clinic    @relation(fields: [clinicId], references: [id])
  plan        Plan      // STARTER | GROWTH | PRO
  status      SubStatus // TRIAL | ACTIVE | PAST_DUE | CANCELED | EXPIRED
  trialEndsAt DateTime?
  renewsAt    DateTime?
  providerId  String?   // Stripe/local PSP reference
  createdAt   DateTime  @default(now())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;SubscriptionGuard&lt;/code&gt; blocks tenant-scoped writes when the clinic's subscription is not in (&lt;code&gt;TRIAL&lt;/code&gt;, &lt;code&gt;ACTIVE&lt;/code&gt;). Reads are still allowed in &lt;code&gt;PAST_DUE&lt;/code&gt; for a grace window — clinics losing patient access mid-day would be unforgivable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 14-day trial is automatic on clinic creation. No credit card. This drove conversion higher than any pricing page change I tried — see &lt;a href="https://tabeeb-hub.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; if you're curious.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with the AsyncLocalStorage tenant context on day one.&lt;/strong&gt; I spent a week refactoring 40+ controllers when I bolted it on later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres row-level security as a belt-and-suspenders layer.&lt;/strong&gt; Application-level scoping is enough operationally, but RLS would catch the one rogue migration script I'd inevitably write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit log every write to &lt;code&gt;Prescription&lt;/code&gt; and &lt;code&gt;Invoice&lt;/code&gt; from day one.&lt;/strong&gt; I added it after a real "who changed this dosage?" support ticket. Should have been there at v1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a queue earlier.&lt;/strong&gt; I shipped appointment reminders as &lt;code&gt;setTimeout&lt;/code&gt; chains. They survived dev. They did not survive a deploy.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. &lt;strong&gt;i18n the database, not just the UI.&lt;/strong&gt; Arabic patient names broke 3 separate PDF generators that assumed Latin-1 widths.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  When this architecture stops working
&lt;/h2&gt;

&lt;p&gt;This shared-DB, tenant-scoped approach is fine until roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~1,000 active clinics, or&lt;/li&gt;
&lt;li&gt;A clinic asks for a &lt;strong&gt;data residency guarantee&lt;/strong&gt; (e.g. a hospital chain that needs an isolated DB), or&lt;/li&gt;
&lt;li&gt;You hit a &lt;strong&gt;noisy neighbor&lt;/strong&gt; query (one giant clinic's reports starve everyone).
At that point you add &lt;strong&gt;read replicas per region&lt;/strong&gt;, then &lt;strong&gt;dedicated DBs for enterprise tenants&lt;/strong&gt;, and keep the shared pool for the long tail. The application code shouldn't change — that's the whole point of the AsyncLocalStorage scoping.
---
## TL;DR&lt;/li&gt;
&lt;li&gt;Treat tenancy as &lt;strong&gt;infrastructure&lt;/strong&gt;, not a feature.&lt;/li&gt;
&lt;li&gt;Scope tenants in the ORM via middleware + AsyncLocalStorage, not in controllers.&lt;/li&gt;
&lt;li&gt;Make &lt;strong&gt;visit status&lt;/strong&gt; a first-class input to your permission system.&lt;/li&gt;
&lt;li&gt;Bill the unit your customer treats as a business (the clinic), not the account.&lt;/li&gt;
&lt;li&gt;Build the boring layers (audit, queues, i18n) early — every one of them I delayed cost me real money later.
If you're building healthcare SaaS in an emerging market, the localization and workflow decisions matter as much as the architecture. Happy to dig into any of those in a follow-up — drop a comment.
---
&lt;em&gt;Abdullah is the founder of &lt;a href="https://tabeeb-hub.com" rel="noopener noreferrer"&gt;TabeebHub&lt;/a&gt; — a cloud clinic management platform for doctors and clinics in Egypt and MENA: appointments, live patient queue, digital prescriptions, patient portal, and billing in Arabic and English.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nestjs</category>
      <category>postgres</category>
      <category>multitenancy</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
