{
  "docVersion": 1,
  "id": "blog-tenancy-0001",
  "name": "Multi-tenancy — one product, tiers of isolation",
  "purpose": "A tenant router sends thousands of small tenants to a pooled tier (shared tables + tenant_id + RLS) and the whale tenant to its own silo database — the isolation ladder as one running system.",
  "nodes": {
    "small": {
      "type": "users",
      "label": "9,999 small tenants",
      "x": 20,
      "y": 100,
      "runtime": "$29/mo each"
    },
    "whale": {
      "type": "user",
      "label": "Tenant W — the whale",
      "x": 20,
      "y": 460,
      "runtime": "40% of revenue",
      "color": "purple"
    },
    "router": {
      "type": "gateway",
      "label": "Tenant Router",
      "x": 300,
      "y": 280,
      "runtime": "tenant → tier lookup",
      "notes": "Resolves every request's tenant to its home tier. This indirection is what lets a tenant MOVE tiers as it grows — the ladder is a lifecycle, not a one-time choice."
    },
    "app": {
      "type": "k8s",
      "label": "App tier (shared)",
      "x": 590,
      "y": 280,
      "runtime": "stateless · per-tenant quotas",
      "patterns": [
        "stateless",
        "autoscale"
      ],
      "notes": "One fleet serves everyone — compute is almost always pooled. Isolation decisions really live in the DATA tier."
    },
    "dbpool": {
      "type": "database",
      "label": "Pooled DB",
      "x": 900,
      "y": 100,
      "color": "blue",
      "runtime": "shared tables · tenant_id · RLS",
      "notes": "Every row carries tenant_id; row-level security enforces the filter even when a query forgets it. Cheapest per tenant, biggest shared blast radius."
    },
    "dbwhale": {
      "type": "database",
      "label": "Whale's own DB",
      "x": 900,
      "y": 460,
      "color": "purple",
      "runtime": "database-per-tenant (silo)",
      "notes": "Own backups, own restore, own quotas, own upgrade window — and their noisy day can't touch anyone else."
    }
  },
  "edges": {
    "e1": {
      "from": "small",
      "to": "router",
      "fromSide": "right",
      "toSide": "left",
      "label": "SaaS traffic"
    },
    "e2": {
      "from": "whale",
      "to": "router",
      "fromSide": "right",
      "toSide": "left",
      "color": "purple"
    },
    "e3": {
      "from": "router",
      "to": "app",
      "fromSide": "right",
      "toSide": "left",
      "animated": true
    },
    "e4": {
      "from": "app",
      "to": "dbpool",
      "fromSide": "right",
      "toSide": "left",
      "label": "WHERE tenant_id = … + RLS",
      "color": "blue",
      "patterns": [
        "rate-limit"
      ]
    },
    "e5": {
      "from": "app",
      "to": "dbwhale",
      "fromSide": "right",
      "toSide": "left",
      "label": "tenant W only",
      "color": "purple",
      "patterns": [
        "bulkhead"
      ]
    }
  },
  "boxes": {
    "b1": {
      "x": 860,
      "y": 20,
      "w": 320,
      "h": 280,
      "label": "pooled tier — economy of scale",
      "color": "blue",
      "opacity": 0.05
    },
    "b2": {
      "x": 860,
      "y": 380,
      "w": 320,
      "h": 280,
      "label": "silo tier — isolation on demand",
      "color": "purple",
      "opacity": 0.05
    }
  },
  "texts": {
    "t1": {
      "x": 20,
      "y": 630,
      "text": "the isolation ladder (cost per tenant rises at every rung):\n1. shared tables + tenant_id — one schema, rows interleaved\n2. schema-per-tenant — same DB, separate namespaces\n3. database-per-tenant — the silo\n4. cell-per-tenant-group — whole stack sharded into cells",
      "color": "green",
      "size": 12
    },
    "t2": {
      "x": 560,
      "y": 80,
      "text": "noisy neighbor: one tenant's batch job\nis everyone's latency spike — quotas\nand rate limits are per-TENANT, not global",
      "color": "amber",
      "size": 12
    }
  },
  "meta": {
    "createdAt": "2026-07-19T09:00:00.000Z",
    "modifiedAt": "2026-07-19T09:00:00.000Z"
  }
}