Spec popisuje, jak TalkIDE provozuje user-generated apps (každý projekt = samostatná Spring Boot + Vue.js aplikace), jaké resources každá aplikace dostává v cílové K8s infrastruktuře, a jak měříme jejich spotřebu pro účely billingu a tier kvót. Cílem dokumentu NENÍ implementace — žádné Kotlin třídy, žádné Helm charty, žádné konkrétní YAMLy. Spec je rozhodovací kotva pro implementační fáze: provisioning model, lifecycle, metering, cost allocation, tier struktura.
Dokument navazuje a doplňuje, ale nepřepisuje:
- architecture.md ADR-001 — control-plane
multi-tenancy přes
tenant_idcolumn platí beze změn; ADR-001 byl aktualizován o data-plane vrstvu (schema-per-app v separátním clusteru) — viz ADR-023. - worker-runtime.md § 5 — shared filesystem strategy pro Mara worker (working tree source kódu na NFS PVC). Tento dokument navazuje na NFS strategii a rozšiřuje ji o per-app runtime data subspace (uploads, generated files).
- per-project-architecture.md — struktura
workspace/output-projects/<slug>/pro source code a.talkide/config. Tento dokument předpokládá, že source code vrstva existuje, a doplňuje runtime hosting vrstvu (kde zabuilděné image z toho source kódu reálně poběží). - ADR-026 + UC-10013 —
Environment first-class koncept. Každý projekt patří do konkrétního prostředí (
DEFAULT“TalkIDE” neboUSER_CREATED). Namespace, ResourceQuota a billing per-env line item navazují na entituenvironment, ne naprojectsamostatně.
1. Kontext a motivace
1.1 Co řešíme
Mara generuje source kód user app, ten se buildí do Docker image, image se deployuje. Otázka: kam, pod jakou identitou, s jakými resource, a jak za to účtujeme?
Konkrétní otázky, které spec uzavírá:
- Provisioning: kdy v lifecycle projektu (create / first deploy / first request) vytvoříme K8s namespace, Postgres DB, NFS subdir?
- Isolation: jak garantovat, že user A nemůže přečíst data user B? Network, FS, DB credentials.
- Resource limits: hard limity, kvóty, tier struktura. Jak vynucujeme?
- Metering: jak měříme storage / compute / DB load / network egress per user, aby billing nebyl odhad, ale faktická spotřeba?
- Lifecycle: idle hibernace, delete s grace period, soft vs. hard delete.
1.2 Co se NEmění oproti existujícím spec
- Cluster topology (jeden K8s cluster
talkide-prodv NYC3) — definováno v rootCLAUDE.md→ DigitalOcean. - Managed Postgres (
talkide-prod-pg, sdílený) jako engine pro per-app DB. - NFS strategy (self-hosted NFS pod backed by DO Block Volume) — ze sidecar specu §2.3.
- Multi-tenant TalkIDE platform DB (ADR-001 row-level isolation) — beze změn pro platform data; tento dokument se týká user app runtime data, což je jiný problém.
1.3 Co je třeba rozhodnout
- Per-app Postgres model (sdílený cluster + per-app DB × per-user dedicated cluster × shared DB
- per-app schema).
- NFS layout (per-app subdir × per-app dedicated PVC).
- K8s isolation (namespace per app × namespace per user × shared namespace s NetworkPolicy).
- Lifecycle moments (create / deploy / hibernate / delete) — kdy vznikají/zanikají resources.
- Metering model (storage by snapshot × compute by pod-hours × DB by stat collector).
- Tier struktura (kvóty, ceny, autohibernate).
2. Resources per user app
Pro každou user app (= jeden záznam v projects tabulce TalkIDE platform DB) alokujeme tři
runtime resources, plus zděděný source code working tree (řešený v per-project-architecture.md
a worker-runtime.md).
2.1 Přehled
| Resource | Forma | Naming (aktuální) | Kdy vzniká | Kdy zaniká |
|---|---|---|---|---|
| K8s namespace | logická hranice pro all pods, services, secrets v daném tenant-environment | {tenant_slug}-{env_slug} (per tenant-env, ne per app) | lazy get-or-create při 1. úkonu v env (UC-10010 EnvironmentService) | tenant deprovision (be#120, open) |
| Postgres schema + role | dedikované schema v cluster B talkide_dataplane | schema = role = tk_t{tenantId}_p{slug}_{env} | create projektu (Liquibase migrations spuštěné přes provisioner direct admin conn) | delete projektu + grace |
| DO Spaces prefix | per-app S3 prefix na bucket talkide-prod-space | apps/user_{userId}/app_{slug}/uploads/, …/generated/ | první upload (lazy) | delete projektu + grace |
| NFS subspace (legacy — viz admonition výše) | subdirectory v talkide-prod-nfs-vol | /exports/user_{userId}/{slug}/ | bylo at create | bylo at delete |
| (zděděno) Working tree | subdirectory workspace/output-projects/{slug}/ na NFS | working tree na NFS PVC | create projektu | delete projektu (immediate) |
2.2 K8s namespace (per tenant-environment)
Každé tenant-environment (např. mirek-dev, mirek-talkide, popelkam-talkide) má vlastní
K8s namespace v clusteru talkide-prod (DO Managed Kubernetes, NYC3).
Co v něm žije (per tenant-environment):
talkide-workerDeployment (1 pod, Node 22 + TS, drží Mara session state na NFS PVC).- Per-projekt Deployments + Services + Ingresses — jeden Deployment per projekt v daném env.
Same-origin single-pod topology: scaffold builduje FE →
dist→COPYdo BE static resources → bootJar, takže jeden pod servíruje API i FE na :8080 (ověřeno #119, viz CLAUDE.md). - K8s Secrets per app: DB connection string (PgBouncer Service endpoint, port 5432, schema search_path už zapečený v role default), Stripe webhook secrets pokud appka má billing.
- K8s Secret
talkide-worker-token(klíčidentity-token,TALKIDE_GATEWAY_BASE_URL) — sdílený workerem v daném ns (viz worker-production.md § 5.2). - PVC
claude-sessions(NFS RWM) — worker session state. - ServiceAccount + RoleBinding scopující batch.Job CREATE/GET/WATCH/DELETE — pro budoucí gradle build dispatch z workeru (ADR-024 § Decision 4, post-alpha).
- Případně ResourceQuota + LimitRange (plan-based; post-alpha).
Co v něm NE-žije:
- Postgres (cluster B
talkide-dataplane-pgmimo K8s). - NFS server pod (
nfs-systemns). - TalkIDE platform components (
talkide-prodns). - PgBouncer pod (žije v
talkidens vedle platform BE — viz ADR-023 § 2).
Routing:
- Per-projekt Ingress matchuje host
<slug>.talkide.app(prod published) nebo<uuid>.talkide.app(DEV preview, per-deploy). FE volá relativní/api(same-origin) — žádnýaxios baseURL, žádnýVITE_API_BASE_URL. Split-origin by byl okamžitý prod-breaker (viz CLAUDE.md “Scaffold architektura”). - BE komunikuje s Postgres přes self-host PgBouncer K8s Service (port 5432) — nikdy přímo na port 25060 cluster B (viz ADR-023 § 6 “Apps mluví vždy na pooler”).
Resource limits per pod se nastavují podle plánu uživatele (vertikální sizing, ADR-024 § Decision 5).
2.3 Postgres schema + role (data-plane cluster B)
Dva fyzicky oddělené DO Managed Postgres clustery (ADR-023 § 4):
- Cluster A —
talkide-prod-pg(control-plane): jedna DBtalkide(tenant metadata, projekty, uživatelé, billing, conversations, activity). Multi-tenant přestenant_idcolumn (ADR-001). Pooler: DO Managed PgBouncer (talkide-txtransaction, size 18 +talkidesession, size 3). - Cluster B —
talkide-dataplane-pg(data-plane): jedna sdílená DBtalkide_dataplanese N schémami per user-app. Pooler: self-host PgBouncer (mainline) v K8s nstalkide(port 5432,auth_type=scram-sha-256,auth_querypřesdataplane_authenticator+dataplane_auth.dataplane_get_authSECURITY DEFINER funkci).
Provisioning per user-app (cluster B, direct admin conn 25060, deploy-time):
Provisioner (PostgresDatabaseProvisioner, ADR-023 § 2.3) provede:
- Vygeneruje random plaintext heslo + spočítá SCRAM-SHA-256 verifier v Kotlinu (RFC 5802, salt ≥ 16 B, ≥ 4096 iterací).
CREATE SCHEMA tk_t{tenantId}_p{slug}_{env}.CREATE ROLE tk_t{tenantId}_p{slug}_{env} WITH LOGIN PASSWORD 'SCRAM-SHA-256$...'— předává verifier verbatim.ALTER ROLE … SET search_path = tk_t{tenantId}_p{slug}_{env}— default role attribute, přežije transaction pooler.REVOKE ALL ON SCHEMA public FROM <role>+ cross-schema revokes (best-effort).INSERT INTO dataplane_auth.credentials(rolname, secret) VALUES (?, ?)— DELETE-then-INSERT pattern, NIKDYON CONFLICT DO UPDATE(#208 LESSON, viz CLAUDE.md).- Plaintext heslo uloží do K8s Secret
app-{slug}-{env}-dbv tenant-env ns.
Connection string v K8s Secret:
SPRING_DATASOURCE_URL=jdbc:postgresql://talkide-pgbouncer.talkide.svc.cluster.local:5432/talkide_dataplane?prepareThreshold=0&sslmode=disable
SPRING_DATASOURCE_USERNAME=tk_t{tenantId}_p{slug}_{env}
SPRING_DATASOURCE_PASSWORD=<plaintext>
prepareThreshold=0 je MUST — server-side prepared statements rozbijí transaction pooler.
Scaffold template má spring.jpa.hibernate.ddl-auto: none (ADR-023 § 3, fix talkide-be#97).
Liquibase migrace běží mimo pooler přes direct admin conn 25060 cluster B (admin
DataSource v BE) — kvůli pg_advisory_lock, který transaction pooler rozbíjí. Spouští se
v deploy-time, ne v init containeru per pod.
Isolation:
- Per-app role má
search_pathzapečený do default attributu → roli při connect okamžitě vidí jen svoje schema. REVOKE ALL ON SCHEMA public FROM <role>+REVOKE ALL ON SCHEMA <other>zabraňuje cross-schema accessu.- Plaintext heslo žije jen v K8s Secret + provisioner momentálně (po výpočtu verifieru). Do
dataplane_auth.credentialsse NIKDY neukládá plaintext ani MD5 — pouze SCRAM verifier (ADR-023 § 2.3 “Kritická invarianta”).
2.4 NFS subspace
Per-app subdirectory v existujícím NFS volume (self-hosted NFS pod, viz CLAUDE.md):
/exports/
├── user_{userId}/
│ ├── {slug}/ ← user app A runtime data
│ └── {slug2}/ ← user app B runtime data
└── ...
Mount do BE pod:
PVC v namespace user app referencuje shared nfs-persistent storageClass s subPath: user_{userId}/{slug}.
Mountuje se do /var/app/data v BE pod.
Účel:
- File uploads (user-uploaded obrázky, dokumenty)
- Generated files (PDF reporty, exporty)
- Cokoliv, co user app potřebuje persistovat mimo DB
Per-app isolation:
- File-level isolation přes UID 1000+ a
subPathmount — BE pod každé app má přístup pouze k vlastnímu subdirectory. - Žádný shared namespace v NFS path treei napříč app.
2.5 Working tree (zděděno)
Source code (Spring Boot kostra, Vue scaffold, vygenerované soubory Mary) žije v:
/workspace/output-projects/{slug}/
Tato vrstva je řešená v per-project-architecture.md a worker-runtime.md § 5. Není runtime data — používá se jen pro build (image build pipeline, ADR-019) a Mara editing. Po úspěšném buildu nemá runtime user app k working tree přístup.
3. Lifecycle
Každý resource má vlastní lifecycle, ne všechny vznikají ve stejném momentu.
3.1 Create project
Trigger: user v UI klikne “Create new project”.
Co TalkIDE BE dělá synchronně:
- Vytvoří
projectsrow v platform DB (statusINITIALIZING). - Vytvoří per-app DB + role (
CREATE DATABASE+CREATE USER). I když user ještě nedeployoval — chceme, aby Mara mohla rovnou plánovat schéma. Budoucí Liquibase changeset jde rovnou do reálné DB, ne do mocku. - Vytvoří NFS subdir (
mkdir -p /exports/user_{userId}/{slug}, chown 1000:1000). - Vytvoří working tree (rsync z scaffold templates do
output-projects/{slug}). - Přepne status na
CREATED.
Co BE NEdělá:
- Nevytváří K8s namespace. Ten vznikne až při prvním deploy. Důvod: namespace bez podů zabírá triviálně málo, ale stejně — jednoduchá invariant “namespace existuje ⇔ projekt někdy byl deployovaný” usnadní debug.
Failure handling:
- Create DB selže (connection error, conflict) → BE rollbackuje project row, vrátí 5xx.
- NFS mkdir selže → BE pokračuje (degraded create), DB resource zachová, project status
CREATED_PARTIAL. User dostane warning.
3.2 Apply version (deploy)
Trigger: user klikne “Apply” na nějakou ProjectVersion (po vibecoding session).
Co TalkIDE BE dělá asynchronně (background job):
- Stáhne working tree pro tu verzi (z git nebo snapshot).
- Build BE image (
docker buildv build pod / GitLab CI runner) → push doregistry.digitalocean.com/talkide/user-{userId}-app-{slug}:v{n}. - Build FE image → push do registry.
- Vytvoří namespace
user-{userId}-app-{slug}(idempotent — pokud existuje, skip). - Aplikuje Liquibase migrace (init container nebo samostatný
Job). - Deploy BE Deployment s image
:v{n}, env vars (DB URL, NFS mount), resource limits podle tieru. - Deploy FE Deployment.
- Vytvoří/aktualizuje Service + Ingress.
- Wait for readiness probes → přepne
ProjectVersion.statusnaAPPLIED, předchozí verze naSUPERSEDED.
Rolling update pro update existujícího deploymentu:
- BE Deployment update → K8s rolling strategy (max surge 1, max unavailable 0) → user nezaznamená downtime.
- DB migrace běží před rolling update (init container na první spouštěné replice s lock-em
via
pg_advisory_lock, nebo dedicatedJobpřed Deployment update). Backward-incompatible migrace jsou v MVP user responsibility (Mara dohlíží, ale netranzakční rollback není součástí tohoto specu).
3.3 Idle / Autohibernate
User apps většinu času čekají bez requestů (typický use case: malý byznys, low traffic). Plné plnění RAM 24/7 je drahé.
Strategie:
- Autohibernate timer per user app (default 15 min idle, tier-dependent, viz §5).
- Idle detection: ingress controller / sidecar exposuje
last_request_atper namespace (Prometheus metric nebo custom controller). TalkIDE platform běží scheduler (každou minutu), který scalujeDeployment.replicas: 0u apps bez requestu déle než tier-limit. - Cold start: na první request po hibernaci ingress vidí 503 (no endpoints) → custom handler u talkide-prod-lb scaluje deployment zpět na 1 a vrátí klienta retry / loading page. Cold start ~5-10 s pro Spring Boot.
- DB + NFS zůstávají alokované — nikdy se nehibernují. Hibernace se týká pouze pod replicas.
Wake-up mechanismus (varianta navrhovaná pro implementaci):
- Custom Ingress middleware: pokud target service má 0 endpoints, odpoví HTTP 503 s
Retry-After: 10, zaroveň pošle event na TalkIDE BE (POST /internal/wake-up), které okamžitě patchneDeployment.replicas=1. - Alternativa: Knative Serving (mature open-source, ale větší dependency).
3.4 Update / new version
Mechanika identická s první deploy (§3.2), jen namespace a DB už existují. Rolling update zajišťuje no-downtime přechod. Liquibase migrace pro novou verzi běží v init container.
3.5 Delete project
Trigger: user v UI klikne “Delete project” + confirmation.
Pořadí cleanup operací (sekvenčně, idempotentně):
| # | Co | Kdy | Důvod |
|---|---|---|---|
| 1 | Scale Deployment.replicas: 0 (BE i FE) | immediate | Zastaví user-facing traffic |
| 2 | Delete K8s namespace (kubectl delete ns ...) | immediate | Cleanup všech ephemeral resources (services, ingress, secrets, configmaps, pods) |
| 3 | Delete Docker images z registry | immediate | Storage savings |
| 4 | Mark NFS subdir read-only (chmod a-w) + add to “pending-delete” queue s timestampem | immediate | Soft delete |
| 5 | Mark Postgres DB read-only (ALTER DATABASE SET default_transaction_read_only = on) + add to “pending-delete” queue | immediate | Soft delete |
| 6 | Po 14 dnech grace period: cron job vymaže NFS subdir (rm -rf) | T+14d | Hard delete s recovery oknem |
| 7 | Po 14 dnech grace period: cron job dropne Postgres DB + role (DROP DATABASE, DROP USER) | T+14d | Hard delete |
| 8 | Working tree (output-projects/{slug}) — delete immediate (žádný grace) | immediate | Source kód lze re-generovat z conversation history pokud user chce; runtime data ne |
Recovery během grace period: User v UI vidí “Recently deleted” sekci s možností “Restore” — re-creates K8s namespace, redeploy poslední APPLIED verze, přepne DB read-write, re-mountne NFS subdir.
Důvody zamítnutí
| Alternativa | Důvod zamítnutí |
|---|---|
| Hard delete immediate (žádná grace period) | Riziko ztráty dat při omylu uživatele. 14 dní = standardní průmyslový recovery window. |
| Soft delete navždy (jen flag v DB) | Postgres cluster a NFS volume bobtnají donekonečna; storage cost roste s deleted apps. |
| Tier-dependent grace period | Komplikuje cron logiku; 14 dní je dost krátké, aby ne-bobtnalo, dost dlouhé, aby zachytilo “ups, smazal jsem omylem”. Uniform pro všechny tiers. |
4. Resource tracking & cost allocation
Klíčová otázka: jak rozpočítat fixní cost sdílené infrastructure (1× K8s cluster, 1× managed Postgres, 1× NFS volume) na konkrétní user apps tak, aby billing odpovídal reálné spotřebě?
4.1 Co se měří
| Resource | Metric | Zdroj | Frekvence | Granularita |
|---|---|---|---|---|
| DB storage | pg_database_size(datname) | Postgres pg_catalog | hourly snapshot | per-DB → per-app |
| NFS storage | du -sb /exports/user_{userId}/{slug} | NFS server pod cron | daily snapshot | per-subdir → per-app |
| Compute (CPU+RAM) | container_cpu_usage_seconds_total, container_memory_working_set_bytes | K8s metrics-server / Prometheus | scrape 30s, aggregate hourly | per-namespace → per-app |
| Pod active hours | kube_pod_status_phase{phase="Running"} | Prometheus | scrape 30s, aggregate hourly | per-namespace → per-app |
| DB connections | pg_stat_activity count by usename | Postgres stat | scrape 60s | per-role → per-app |
| DB query time | pg_stat_database.{tup_returned, blks_read, blks_hit} | Postgres stat | scrape 60s, delta hourly | per-DB → per-app |
| Network egress | NetworkPolicy + Prometheus (container_network_transmit_bytes_total) | K8s | scrape 30s, aggregate hourly | per-namespace → per-app |
Storage from snapshot, not stream: storage je easy — vezmi velikost teď, fakturuj za GB-měsíc. Není potřeba sledovat každý write.
Compute from cumulative counters: pod-hours a CPU-seconds jsou z monotonicky rostoucích
counterů — TalkIDE BE je čte z Prometheus přes /api/v1/query_range jednou za hodinu a ukládá
delta do platform DB tabulky usage_records.
4.2 Cost allocation model
Doporučená varianta: Hybrid — flat tier price + metered overage.
Každý tier má inclusive bundle resources zdarma (např. Pro tier: 100 pod-hours/měs, 10 GB storage). Když user překročí, fakturuje se overage podle metered jednotek.
Důvody:
- Predictable billing pro user: většina users zůstane v inclusive bundle, billing je fixní měsíční částka.
- Power users platí adekvátně: kdo žene 24/7 traffic + 1TB storage, neutopí systém v fixní subscription.
- Free tier ≠ metered: na free tier je metering přepych. Místo toho hard quotas — překročíš limit, app se vypne (HTTP 503 + email). Žádný overage billing.
| Tier | Pricing model | Inclusive | Overage |
|---|---|---|---|
| Free | $0 / měs | hard quotas (viz §5) | žádný overage; přes limit = app vypnutá |
| Starter | flat $X / měs | tier kvóty | žádný overage v MVP (alert na 100% → upgrade prompt) |
| Pro | flat $Y / měs + metered | tier kvóty | $/GB-měsíc storage, $/pod-hour, $/GB egress |
| Enterprise | smluvní | smluvní | smluvní |
Konkrétní ceny budou stanovené business týmem při GA launch — tento spec definuje framework, ne ceník.
4.3 Aggregation a fakturace
Pipeline:
-
Hourly aggregator (TalkIDE platform cron) čte raw metriky z Prometheus / Postgres / NFS cron a zapisuje do
usage_recordstabulky:usage_records: id, project_id, user_id, period_start, period_end, metric_type, value, unitmetric_type ∈
{}db_storage_gb_hours, nfs_storage_gb_hours, pod_cpu_seconds, pod_memory_gb_hours, network_egress_bytes, db_connections_max, db_query_seconds{}. -
Daily roll-up: aggregátor sumarizuje
usage_recordsdousage_daily(jeden řádek per project per day per metric_type). Účel: rychlé dashboardy, billing performance. -
Monthly billing run: 1. den měsíce TalkIDE platform spočítá z
usage_dailycelkovou spotřebu za předchozí měsíc, porovná s tier inclusive bundle, vygeneruje invoice s overage položkami. -
Real-time soft warnings: každou hodinu kontroluje aktuální spotřebu vs. tier limit:
- 80% → email warning, banner v UI (“App approaches Pro tier compute limit”).
- 100% → soft cutoff (podle tier policy):
- Free tier: hard cutoff, app scale-to-zero, email “upgrade or wait until next month”.
- Paid tiers: soft warning, app běží dál, overage se naúčtuje příští faktura.
4.4 Důvody zamítnutí
| Alternativa | Důvod zamítnutí |
|---|---|
| Pure metered (žádné tiery) | User nechce každý měsíc překvapenou fakturu. Predictability je core value pro non-technical target audience. |
| Pure flat (žádný metering) | Power user vs. light user platí stejně → křivá ekonomika. Free riders dotují platforma-cost. |
| Per-second compute billing (Lambda-style) | Spring Boot není serverless-friendly (cold start 5-10s). Per-second granularita je pro pod-based deployment přepych a komplikuje aggregaci. Pod-hours stačí. |
| DB query-count billing | Velmi obtížné pro user pochopit (“co je query?”). Místo toho měříme storage + connections + cumulative query time jako proxy. |
| Žádné metering, jen hard quotas pro všechny | Enterprise zákazníci očekávají usage-based billing. Bez něj nelze nabídnout enterprise tier. |
5. Quotas a tiers
5.1 Tier struktura
| Limit | Free | Starter | Pro | Enterprise |
|---|---|---|---|---|
| Max projektů (apps) | 1 | 3 | 10 | smluvní |
| Max DB size per app | 100 MB | 1 GB | 10 GB | smluvní |
| Max NFS storage per app | 500 MB | 5 GB | 50 GB | smluvní |
| Max pod-hours / měs (active runtime, summed across apps) | 30 h (~1 h/day) | 200 h | 720 h (= 24×30, “always-on”) | smluvní |
| Max concurrent DB connections per app | 5 | 10 | 25 | smluvní |
| Max bandwidth egress / měs | 1 GB | 50 GB | 500 GB | smluvní |
| Autohibernate idle timeout | 5 min | 15 min | 30 min | konfigurovatelné |
| Cold start tolerance | 5-10 s (acceptable) | 5-10 s | 5-10 s | option to disable hibernace |
| Custom domain | NE | ANO (1 per app) | ANO (5 per app) | unlimited |
| Backup retention | žádný | 7 dní daily | 30 dní daily + weekly | konfigurovatelné |
| Support | community | email 48h | email 12h + chat | dedicated SLA |
Klíčové vlastnosti tier modelu:
- Pod-hours sečtené napříč všemi apps usera, ne per-app. Důvod: multi-app user na Pro tier může mít 3 apps běžící 8 h/den nebo 1 app 24/7 — stejný pricing.
- Storage je per-app (DB i NFS). Důvod: storage je clean, nesdílí se mezi apps; per-app limit zabraňuje single runaway app spotřebovat celou kvótu.
- Free tier autohibernate 5 min — pokud chceš dlouhodobý uptime, upgraduj.
- Backup retention je tier-feature, ne metering — backup je infrastructure cost, ne user cost (viz §6 Future work).
5.2 Vynucování kvót
Hard quotas (technicky vynucované, app se zastaví):
- Max DB size: Postgres
ALTER DATABASE WITH CONNECTION LIMIT+ monitoring; při překročení read-only mode + email. - Max NFS storage:
quotasystem call NEBO dailyducheck + read-only mount. - Max projektů: TalkIDE BE refusne
Create projectrequest. - Max DB connections:
ALTER USER WITH CONNECTION LIMIT N.
Soft quotas (warn-only):
- Pod-hours, bandwidth: warning na 80%, action na 100% podle tier policy (free = cutoff, paid = overage billing).
Tier upgrade flow:
- User klikne “Upgrade” → platba → tier se přepne v platform DB → kvóty re-applied (Postgres ALTER, K8s ResourceQuota update) → user může pokračovat v práci.
5.3 Důvody zamítnutí
| Alternativa | Důvod zamítnutí |
|---|---|
| Per-app pod-hours limit | Penalizuje multi-app users; user s 3 apps × 8 h/day by potřeboval 3× větší tier než user s 1 app × 24 h/day, přestože spotřeba je stejná. Sečteno přes account je férovější. |
| Žádný free tier | Free tier je kritický pro acquisition; non-technical users chtějí “yzkouset” před commitnutím. Hard quotas drží free tier ekonomicky únosný. |
| Unlimited storage | Storage je nelineární cost (NFS volume size, Postgres disk size mají hard ceiling). Bez limitů hrozí abuse (user nahraje 100 GB photo backup). |
6. Architectural Decision Records
ADR-101: Per-app Postgres DB ve sdíleném managed clusteru
Status: Superseded by ADR-023 — Schema-per-app + dva fyzicky oddělené PG clustery (2026-05-16, LIVE) Date: 2026-05-06
Context: Každá user app potřebuje vlastní DB. Možnosti: (a) sdílený cluster + per-app DB; (b) sdílená DB + per-app schema; (c) per-user dedicated cluster.
Decision: (a) sdílený cluster + per-app DB.
Důvody:
- Isolation per DB: Postgres role + DB-level GRANT poskytují čistou isolation. Cross-DB query nejsou možné bez cross-DB connection (kterou role nemá).
- Backup/restore granularita: per-DB backup je standard (
pg_dump); per-schema je složitější (musí backupovat schema + role + grants koherentně). - Sizing flexibility: managed cluster škáluje vertikálně, ne nutně přidávat clustery. Začínáme na Basic 1GB/1vCPU/10GB, scale-up online (CLAUDE.md DigitalOcean).
- Cost efficiency: 1 managed cluster = 1 fixed cost; per-user dedicated by násobil N× minimum sizing fee.
Důvody zamítnutí alternatives:
| Alternativa | Důvod zamítnutí |
|---|---|
| (b) Shared DB + per-app schema | Cross-tenant data leakage je 1 SQL bug daleko (chybný WHERE schema_name=...). Schema-level GRANT je crackpot oproti DB-level GRANT. Backup granularita horší. |
| (c) Per-user dedicated cluster | Minimální cluster = $15/měs (DO Basic). Pro free tier je to economically nemožné. Operations overhead × N. |
| Per-app dedicated cluster | Násobí (c) ještě o faktor “počet apps per user”. Ekonomická sebevražda. |
ADR-102: NFS subdirectories per app, ne dedicated PVC
Status: Accepted Date: 2026-05-06
Context: User app potřebuje persistent filesystem (uploads, generated files). Možnosti: (a) shared NFS volume + per-app subdir; (b) dedicated PVC per app.
Decision: (a) shared NFS volume + per-app subdir s subPath mount.
Důvody:
- Operational simplicity: jeden NFS server pod, jeden DO Block Volume backing, jeden storage class. Neexpandujeme PVC count s počtem app.
- Resize flexibility: NFS volume rozšířím jednou pro celý cluster, ne per-app PVC × N.
- Per-app isolation:
subPathmount + UID 1000+ + non-root pod = adequate isolation pro multi-tenant SaaS. Není to vojenská isolation, ale na MVP postačí. - Konzistence s worker specem: worker NFS PVC
claude-sessions(worker-runtime.md § 5) také používá per-conversation subdir pattern. Stejný pattern pro app runtime data minimalizuje surprise.
Důvody zamítnutí:
| Alternativa | Důvod zamítnutí |
|---|---|
| Dedicated PVC per app | Každá PVC v K8s je discrete object (DO Block Volume per PVC = $1+/měs minimum). Ekonomicky horší pro malé apps; bobtná state v K8s API. |
| S3 / Spaces object storage | User Spring Boot apps obecně očekávají POSIX filesystem. S3 mount (FUSE) má známé problémy s mtime, atomicity, build toolchain — viz sidecar §2.3. |
| Block storage + ext4 per app | RWO-only; pod restart na jiný node = mount delay; nepodporuje multi-replica BE deploy. |
ADR-103: Hosting cost allocation = hybrid flat + metered overage
Status: Superseded by postpaid hosting model (UC-10009 ENVIRONMENT REVISION, ADR-026) Date: 2026-05-06
Context: Sdílená infrastructure cost (cluster, Postgres, NFS) musí být rozpočítaná na users tak, aby (i) free tier nebyl neudržitelný, (ii) heavy users platili adekvátně, (iii) billing byl pro non-technical users srozumitelný.
Decision: Tiered subscription (flat $/měs) + metered overage pro Pro+ tier nad inclusive bundle. Free tier = hard quotas, žádný overage. Detail viz §4.2.
Důvody: viz §4.4 (alternativy zamítnuty).
Consequences:
- TalkIDE platform musí provozovat metering pipeline (Prometheus → hourly aggregator →
usage_recordstabulka → monthly billing). - Tier upgrade flow musí být frictionless (in-app upgrade, immediate quota update).
- Risk: power user na Free tier vyždímá CPU 1 h/day a je free forever. Akceptujeme — 1 h/day je < $0.50/měs cost, acquisition value > cost.
ADR-104: Autohibernate scale-to-zero pro idle apps
Status: Accepted Date: 2026-05-06
Context: User apps většinu času nemají traffic. Stálé udržování pod replicas = zbytečný compute cost.
Decision: Scale Deployment.replicas: 0 po N minutách bez requestu, kde N je tier-dependent
(Free 5 min, Starter 15 min, Pro 30 min). Cold start ~5-10 s na první request je akceptovatelný
trade-off. DB + NFS zůstávají alokované (žádný hibernate-storage). Detail viz §3.3.
Důvody:
- Compute savings: malá user app (~800 MB BE pod) v hibernaci uvolňuje ~1 GB RAM cluster capacity → více apps na stejné node count.
- Tier differentiator: vyšší tier = delší idle timeout = lepší UX (méně cold starts).
- DB connections šetří: idle pod nedrží connection pool → uvolňuje connection slot pro ostatní apps.
Důvody zamítnutí:
| Alternativa | Důvod zamítnutí |
|---|---|
| Žádná hibernace, always-on | Compute cost roste lineárně s počtem apps; ekonomicky neudržitelné na Free tier. |
| Hibernace včetně DB | DB hibernace u managed Postgres není feature; DROP+RESTORE per hibernace cyklus by zničil performance a data. |
| Knative Serving | Mature, ale heavy dependency (samostatný control plane). Custom Ingress middleware (§3.3) je 200 řádků kódu, dostatečné pro MVP. |
ADR-105: Naming convention user_{userId}_app_{slug} pro DB a namespace
Status: Superseded by ADR-023 § 1 (schema/role naming) + ADR-015 (namespace naming) Date: 2026-05-06
Context: Per-app resource naming musí být deterministické, sortable, parsable z TalkIDE BE, human-readable pro DevOps debug.
Decision: Vzor user_{userId}_app_{slug} pro Postgres DB+role a user-{userId}-app-{slug}
pro K8s namespace (K8s neumožňuje underscore v namespace).
Důvody:
- Userid (numeric) garantuje uniqueness i pokud user smaže projekt a vytvoří nový se stejným slugem (slug může být reused; user_id je permanent).
- Slug je human-readable pro DevOps grep.
- Prefix
user_odděluje user-app DB od platform DB (talkide) v\llistu.
Consequences:
- Slug musí splňovat K8s naming rules (lowercase, alphanumeric, dash) — TalkIDE BE validuje při create.
- Maximum length: K8s namespace = 63 chars;
user-XXXXX-app-prefix = ~16 chars → slug ≤ 47 chars. Validovat při create projektu.
ADR-106: Delete s 14denním grace period pro DB a NFS, immediate pro K8s a registry
Status: Accepted Date: 2026-05-06
Context: User omylem smaže projekt; potřebuje recovery window. Současně držet smazaná data forever bobtná storage cost.
Decision: 14 dní grace period pro DB (read-only) a NFS subdir (read-only). K8s namespace, registry images a working tree mažou immediate (jsou re-creatable z DB + version history). Detail viz §3.5.
Důvody:
- 14 dní = standard recovery window napříč SaaS (GitHub, Heroku, AWS).
- DB + NFS = unrecoverable data (user-generated content) → grace.
- K8s + registry + working tree = derived state → re-creatable, no grace.
- User UI “Recently deleted” sekce poskytuje self-service recovery bez support ticketu.
7. Open questions / future work
Otázky, na které tato spec neodpovídá, ale jsou důležité pro implementační fáze nebo pro vyšší tiery:
7.1 Provoz a infrastruktura
- Custom domains (Pro+ tier): user chce
myapp.commístomyapp.apps.talkide.app. Potřebuje DNS validation flow, Let’s Encrypt automation per-app cert, SNI routing v Ingress. Otevřená otázka: jaká je správa cert renewals a kdo platí ACM/Let’s Encrypt rate limits. - BYOC (Bring Your Own Cloud) pro Enterprise: zákazník chce, aby user app běžely v jeho AWS/GCP účtu. Vyžaduje GitOps deployment do externího K8s clusteru, federated metering, nový billing model. Out-of-scope MVP.
- Multi-region failover pro Pro+ tier: aktivně-pasivní replikace DB do druhého regionu, geo-DNS, RTO/RPO targets. Závisí na DigitalOcean managed Postgres replication features (cross-region replicas).
- Database backups per user app: tier feature (§5.1). Otevřená otázka — používáme managed
Postgres point-in-time recovery (cluster-wide) nebo per-DB
pg_dumpcron? PITR je clusterové, nelze restore jen jedné user-app DB bez ovlivnění ostatních. - Encryption at rest: managed Postgres + DO Block Volume už mají native encryption (AES-256 at rest). Ale chceme per-tenant encryption keys (KMS) pro Enterprise compliance? Out-of-scope MVP, tier feature.
- Encryption in transit mezi BE pod a Postgres: managed Postgres vyžaduje TLS, Spring Boot
datasource musí mít
sslmode=require. Existující default — k ověření při deploy templating. - NFS performance pro user apps: self-hosted NFS pod backed by single Block Volume = single point of bottleneck. Při 100+ apps čtení/zápis na NFS současně může být degradace. Monitoring threshold a migration plan na DO managed NFS (až bude v NYC3) je open.
7.2 Metering a billing
- Anti-abuse: jak detekovat user, který provozuje cryptominer v BE pod? CPU usage detection
- automatic suspension. Out-of-scope MVP, ale na launch před GA potřeba.
- Bandwidth metering granularity: per-user vs per-end-user-of-user-app. Pokud user má veřejnou app, bandwidth platí user (= TalkIDE customer), ne jeho návštěvníci. Tato spec to předpokládá; otevřená otázka pro abuse case (DDoS na user app → user dostane fakturu).
- Liquibase migrace failure recovery: nová verze má broken migraci, init container failne → deployment stuck. Co s tím — automatický rollback na předchozí verzi, manuální DBA intervence, UI tlačítko “rollback migration”? Out-of-scope tato spec, patří do ProjectVersion lifecycle spec.
- Per-app rate limiting: Pro tier má 25 connections k DB. Co se stane při 26.? Postgres refuses → BE pod 5xx → user vidí error. Chce se queue / backpressure / degraded mode? Otevřená otázka, MVP = hard refuse.
- Billing currency a tax: USD-only MVP nebo multi-currency? VAT/sales tax per region? Business decision, ne technický spec.
7.3 Bezpečnost a compliance
- GDPR data residency: NYC3 = US region. EU customers můžou požadovat EU hosting. Future: AMS3 region pool, per-tenant region selection při signupu.
- Audit log per user-app resource changes: kdo kdy provedl deploy / delete / quota change?
Pro Enterprise compliance. Žije v platform DB (
audit_eventstabulka), ale spec to neřeší. - Disaster recovery testing: jak často testujeme restore z backupu? RPO/RTO targets per tier. Out-of-scope MVP.
- Network policy mezi user app namespaces: default deny + explicit allow do NFS namespace a Postgres VPC. Otevřená otázka konkrétní policy YAML — patří do implementační fáze.
8. FEEDBACK
(Tato sekce není součástí dokumentu — je určená PM jako zpětná vazba k zadání.)
Co bylo jasné: zadání mělo perfektně rozkreslené sekce (Resources / Lifecycle / Tracking /
Quotas / ADR / Open questions) — to je nejlepší způsob, jak držet spec konzistentní napříč
všemi rozhodnutími. Rozpis tří resource typů (K8s ns / Postgres DB / NFS subspace) byl jasný
a postavil základ pro celý zbytek dokumentu. Existující architecture.md (ADR-001) +
worker-runtime.md § 5 (NFS strategy) + root CLAUDE.md (DigitalOcean naming) daly dost
context, abych nemusel hádat infra rozhodnutí.
Co chybělo / komplikovalo: konkrétní ceny tier kvót (kolik $ za Starter / Pro) — formuloval jsem framework, ne ceník, protože to je business decision. Druhá komplikace: business model billing decision (USD-only? VAT? credits vs. invoices?) — nechal jsem v Open questions, ale ovlivňuje implementaci §4.3 pipeline. Třetí: ne úplně jasné, jestli je v MVP scope také multi-replica per user app nebo jen single-replica. Default jsem zvolil single-replica (autohibernate scale 0↔1), multi-replica je implicit Enterprise feature v Open questions.
Co bych eskaloval na PM:
- 7.1.1 (custom domains) a 7.1.4 (DB backups) jsou tier-features, které ovlivňují §5 quotas table — bez business decision o těchto features je tier “Pro” trochu spekulativní.
- 7.2.8 (anti-abuse cryptominer detection) — pre-launch must-have, jinak Free tier zničí cluster ekonomiku během prvních dnů.
- 7.3.13 (GDPR / EU region) — pokud target trh není jen US, je to launch blocker pro EU customers.
Rozpor v existujících speccích (historicky stav 2026-05-06): žádný. ADR-001 (multi-tenant via tenant_id) se týkal jen platform DB; tato spec se týká user-app runtime resources — nezasahuje do původního ADR-001 scope. NFS strategy (self-host pod) ze sidecar-production (nyní worker-runtime.md § 5) a CLAUDE.md byla konzistentní; tato spec ji rozšiřovala o per-app subdir layout pro runtime data, který sidecar spec nepokrýval.
(Pozn. 2026-05-23: dokument byl od té doby revidován pro ADR-023 schema-per-app + ADR-026 environment-first-class + DO Spaces runtime storage — viz admoniční bloky výše. Postpaid billing model nahradil původní subscription tiers — viz UC-10009 Postpaid Hosting.)
Thanks for the feedback.