← EP21 Internal Docs Podpora21.cz
ADR · ADR-0002 · v1.0.0

Multi-tenant Isolation Model

accepted v1.0.0 Authors: david.sorf, claude-sonnet-4-6 Created: 2026-04-10 Updated: 2026-04-10
tenant multi-tenant db isolation saas architecture
Podpora21 používá row-level tenant isolation — každá tenant-owned tabulka obsahuje sloupec tenant_id (FK → core_tenant). Odmítnuto: schema-per-tenant (provozní složitost) a DB-per-tenant (neúměrné náklady pro SaaS). Fáze 1 je single-tenant s TenantResolver ze statické konfigurace; fáze 2 rozšiřuje na subdomain routing bez DB schema změn.
Related docs:
Related tasks:

Kontext

Podpora21.cz je SaaS helpdesk — v první fázi provozovaný pro EP21, v budoucnu potenciálně pro další zákazníky. Architekturální rozhodnutí o tenant izolaci musí být přijato před implementací DB schema, protože zpětná migrace je nákladná.

Klíčová otázka: Jak izolovat data mezi tenanty, aby:

Uvažované možnosti:

A) Row-level isolation — tenant_id sloupec na každé tenant-owned tabulce, WHERE tenant_id = ? v každé query

B) Schema-per-tenant — každý tenant má vlastní PG schema (ep21.support_ticket, acme.support_ticket)

C) Database-per-tenant — každý tenant má vlastní PostgreSQL databázi

D) Application-level sharding — separátní aplikační instance per tenant (Docker container per tenant)

Rozhodnutí

✓ Chosen: A — Row-level isolation přes tenant_id

Zvolena Option A (row-level) z těchto důvodů:

1. Jeden PG cluster, jedno schema podpora21 — minimální provozní overhead

2. Přidání tenanta = INSERT do core_tenant — žádné DDL operace

3. Konzistentní s existující architekturou (Nette\ Database, Phinx migrace)

4. Dobře testovatelné — WHERE tenant_id = ? je explicitní a auditovatelné

5. RLS (Row Level Security) je volitelné rozšíření — dá se přidat bez schema změn

6. Search path podpora21,public zůstává beze změny

Proč ostatní odmítnuty:

B) Schema-per-tenant — složitá správa Phinx migrací (N migrací pro N tenantů), pg_dump per schema, search_path je mutable (bezpečnostní risk)

C) DB-per-tenant — neúměrné náklady pro SaaS s malým počtem tenantů, složitý connection pooling, každý tenant vyžaduje vlastní Docker PG container

D) Separate app instance — duplicita Docker stacku, sdílení session/auth je nemožné, update = deploy N instancí

Fáze 1 vs. fáze 2:

  • Fáze 1: single-tenant (EP21), TenantResolver čte TENANT_KEY z .env — nulová routing logika
  • Fáze 2: multi-tenant, TenantResolver resolví z HTTP_HOST subdomény (ep21.podpora21.cz → tenant 'ep21') — žádné DB schema změny potřeba, pouze nový resolver

Bezpečnostní garance:

  • Každá query na tenant-owned tabulku MUSÍ obsahovat WHERE tenant_id = $tenant->id
  • TenantResolverService je injected do všech Presenterů a Modelů přes DI
  • Code review pravidlo: query na tenant tabulku bez tenant_id filtru = blokátor

Důsledky

Pozitivní:

Negativní / rizika:

Mitigace: code review checklist, BaseModel helper metoda withTenant()

Mitigace: composite index (tenant_id, created_at) na support_ticket

Mitigace: admin skripty s explicit tenant_id parametrem

Neutrální:

Trigger pro přechod na fázi 2 (multi-tenant routing)

Fáze 2 (subdomain routing) se spustí když:

Co je potřeba pro fázi 2 (bez DB schema změn):

1. TenantResolverService: přidat subdomain resolver (HTTP_HOST parsing)

2. RouterFactory: přidat tenant context do URL generation

3. Nginx/Apache: wildcard subdomain routing

4. SSL: wildcard certifikát (*.podpora21.cz)

5. Seed script: nový tenant onboarding

Toto NENÍ v scope aktuálního sprintu — zaznamenáno pro budoucí plánování.

Changelog

VersionDateAuthorNote
1.0.02026-04-10david.sorf + claude-sonnet-4-6

Initial accepted version — row-level isolation zvolen, schema/DB-per-tenant odmítnuty, fáze 1 single-tenant, fáze 2 subdomain trigger definován.