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

Public Portal Architecture and Tenant Model

accepted v1.0.0 Authors: david.sorf, claude-sonnet-4-6 Created: 2026-04-10 Updated: 2026-04-10
portal public tenant architecture client-access
Podpora21 odděluje veřejný klientský portál od interního helpdesk backendu; v první fázi běží jako single-tenant instance pro EP21 s připravenou DB vrstvou pro budoucí multi-tenant rozšíření.
Related docs:
Related tasks:

Kontext

Podpora21.cz je zákaznická podpora EP21. Přistupují k ní dva různé typy uživatelů s odlišnými potřebami:

1. Agenti (interní EP21 pracovníci) — potřebují plný helpdesk backend s ticketingem, statistikami, správou

2. Klienti (zákazníci a studenti EP21) — potřebují jednoduchý portál pro podání požadavku a sledování stavu

Dosud (po bootstrapu) je aplikace čistě interní — bez veřejné landing page, bez klientského přístupu. Nepřihlášený uživatel vidí jen přihlašovací formulář.

Zároveň má podpora21 potenciál jako SaaS — EP21 by mohlo provozovat helpdesk i pro jiné zákazníky (multi-tenant). Tuto vrstvu je třeba architekturálně rozhodnout teď, aby budoucí rozšíření nevyžadovalo breaking changes.

Uvažované možnosti:

A) Shared single URL space — vše na jedné doméně, role řídí co vidíš

B) Subdomain separation — klienti na client.podpora21.cz, agenti na admin.podpora21.cz

C) Path-based separation — /portal pro klienty, /helpdesk pro agenty, / jako veřejná landing

D) Separate app — klientský portál jako samostatná Nette aplikace

Rozhodnutí

✓ Chosen: C — Path-based separation v rámci jedné aplikace

Zvolena Option C (path-based separation) z těchto důvodů:

1. Jedna Nette aplikace, jeden deployment — minimální provozní overhead

2. Sdílené DI services, modely a databázové připojení — žádná duplicita

3. URL struktura jasně komunikuje kontext: / = veřejné, /portal = klient, /helpdesk = agent

4. RBAC řídí přístup — není třeba oddělovat subdomény

5. Konzistentní s existující architekturou (BasePresenter, RouterFactory)

6. Multi-tenant ready: path-based separation je kompatibilní s budoucím subdomain routingem — /portal URL zůstane stejná, jen TenantResolver přidá subdomain logiku (viz ADR-0002)

Proč ostatní odmítnuty:

A) Shared single space — matoucí UX, těžší RBAC bez jasné URL hranice

B) Subdomain separation — v první fázi zbytečná složitost; přidat ji jako rozšíření TenantResolveru je jednodušší než od začátku

D) Separate app — duplicita kódu, dva deployments, sharing session/auth je složité

Multi-tenant jako design constraint (ne afterthought):

Tenant izolace je constraint platný od fáze 1 — viz ADR-0002. tenant_id je přidán na všechny tenant-owned tabulky před prvním deploymentem. TenantResolverService je DI singleton injected do presenterů a modelů. V fázi 1 je resolver statický (TENANT_KEY=ep21 z .env). Přidání druhého tenanta nevyžaduje DB schema změny — pouze nový core_tenant řádek a nový TENANT_KEY (nebo subdomain v fázi 2). Viz DS-0003 pro technický detail.

Důsledky

Pozitivní:

Negativní / rizika:

Neutrální:

Routing contract

RouterFactory musí deklarovat public routes PŘED autentizovanými:

Public (bez autentizace):

GET / → Homepage:default

GET /portal → Portal:default

GET /portal/register → Portal:register

POST /portal/quick-ticket → Portal:quickTicket (signal nebo action)

GET /log/in → Log:in

POST /log/in → Log:in (login form)

GET /log/out → Log:out

Client (role: client):

GET /portal/dashboard → Portal:dashboard

GET /portal/ticket/<id> → Portal:ticket

Agent (role: agent, admin):

GET /helpdesk → Ticket:dashboard

/ticket/* → Ticket:*

/rbac-admin/* → RbacAdmin:*

/tek/* → Tek:*

Changelog

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

Initial accepted version — path-based separation, single-tenant fáze 1, multi-tenant fáze 2 future.