Internal Documentation internal
TalkIDE internal documentation

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_id column 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” nebo USER_CREATED). Namespace, ResourceQuota a billing per-env line item navazují na entitu environment, ne na project samostatně.

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á:

  1. Provisioning: kdy v lifecycle projektu (create / first deploy / first request) vytvoříme K8s namespace, Postgres DB, NFS subdir?
  2. Isolation: jak garantovat, že user A nemůže přečíst data user B? Network, FS, DB credentials.
  3. Resource limits: hard limity, kvóty, tier struktura. Jak vynucujeme?
  4. Metering: jak měříme storage / compute / DB load / network egress per user, aby billing nebyl odhad, ale faktická spotřeba?
  5. 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-prod v NYC3) — definováno v root CLAUDE.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

  1. Per-app Postgres model (sdílený cluster + per-app DB × per-user dedicated cluster × shared DB
    • per-app schema).
  2. NFS layout (per-app subdir × per-app dedicated PVC).
  3. K8s isolation (namespace per app × namespace per user × shared namespace s NetworkPolicy).
  4. Lifecycle moments (create / deploy / hibernate / delete) — kdy vznikají/zanikají resources.
  5. Metering model (storage by snapshot × compute by pod-hours × DB by stat collector).
  6. 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

ResourceFormaNaming (aktuální)Kdy vznikáKdy zaniká
K8s namespacelogická 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 + rolededikované schema v cluster B talkide_dataplaneschema = 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 prefixper-app S3 prefix na bucket talkide-prod-spaceapps/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 createbylo at delete
(zděděno) Working treesubdirectory workspace/output-projects/{slug}/ na NFSworking tree na NFS PVCcreate projektudelete 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-worker Deployment (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 → distCOPY do 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-pg mimo K8s).
  • NFS server pod (nfs-system ns).
  • TalkIDE platform components (talkide-prod ns).
  • PgBouncer pod (žije v talkide ns 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 DB talkide (tenant metadata, projekty, uživatelé, billing, conversations, activity). Multi-tenant přes tenant_id column (ADR-001). Pooler: DO Managed PgBouncer (talkide-tx transaction, size 18 + talkide session, size 3).
  • Cluster B — talkide-dataplane-pg (data-plane): jedna sdílená DB talkide_dataplane se N schémami per user-app. Pooler: self-host PgBouncer (mainline) v K8s ns talkide (port 5432, auth_type=scram-sha-256, auth_query přes dataplane_authenticator + dataplane_auth.dataplane_get_auth SECURITY DEFINER funkci).

Provisioning per user-app (cluster B, direct admin conn 25060, deploy-time):

Provisioner (PostgresDatabaseProvisioner, ADR-023 § 2.3) provede:

  1. Vygeneruje random plaintext heslo + spočítá SCRAM-SHA-256 verifier v Kotlinu (RFC 5802, salt ≥ 16 B, ≥ 4096 iterací).
  2. CREATE SCHEMA tk_t{tenantId}_p{slug}_{env}.
  3. CREATE ROLE tk_t{tenantId}_p{slug}_{env} WITH LOGIN PASSWORD 'SCRAM-SHA-256$...' — předává verifier verbatim.
  4. ALTER ROLE … SET search_path = tk_t{tenantId}_p{slug}_{env} — default role attribute, přežije transaction pooler.
  5. REVOKE ALL ON SCHEMA public FROM <role> + cross-schema revokes (best-effort).
  6. INSERT INTO dataplane_auth.credentials(rolname, secret) VALUES (?, ?) — DELETE-then-INSERT pattern, NIKDY ON CONFLICT DO UPDATE (#208 LESSON, viz CLAUDE.md).
  7. Plaintext heslo uloží do K8s Secret app-{slug}-{env}-db v 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_path zapeč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.credentials se 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 subPath mount — 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ě:

  1. Vytvoří projects row v platform DB (status INITIALIZING).
  2. 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.
  3. Vytvoří NFS subdir (mkdir -p /exports/user_{userId}/{slug}, chown 1000:1000).
  4. Vytvoří working tree (rsync z scaffold templates do output-projects/{slug}).
  5. 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):

  1. Stáhne working tree pro tu verzi (z git nebo snapshot).
  2. Build BE image (docker build v build pod / GitLab CI runner) → push do registry.digitalocean.com/talkide/user-{userId}-app-{slug}:v{n}.
  3. Build FE image → push do registry.
  4. Vytvoří namespace user-{userId}-app-{slug} (idempotent — pokud existuje, skip).
  5. Aplikuje Liquibase migrace (init container nebo samostatný Job).
  6. Deploy BE Deployment s image :v{n}, env vars (DB URL, NFS mount), resource limits podle tieru.
  7. Deploy FE Deployment.
  8. Vytvoří/aktualizuje Service + Ingress.
  9. Wait for readiness probes → přepne ProjectVersion.status na APPLIED, předchozí verze na SUPERSEDED.

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 dedicated Job př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_at per namespace (Prometheus metric nebo custom controller). TalkIDE platform běží scheduler (každou minutu), který scaluje Deployment.replicas: 0 u 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ě patchne Deployment.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ě):

#CoKdyDůvod
1Scale Deployment.replicas: 0 (BE i FE)immediateZastaví user-facing traffic
2Delete K8s namespace (kubectl delete ns ...)immediateCleanup všech ephemeral resources (services, ingress, secrets, configmaps, pods)
3Delete Docker images z registryimmediateStorage savings
4Mark NFS subdir read-only (chmod a-w) + add to “pending-delete” queue s timestampemimmediateSoft delete
5Mark Postgres DB read-only (ALTER DATABASE SET default_transaction_read_only = on) + add to “pending-delete” queueimmediateSoft delete
6Po 14 dnech grace period: cron job vymaže NFS subdir (rm -rf)T+14dHard delete s recovery oknem
7Po 14 dnech grace period: cron job dropne Postgres DB + role (DROP DATABASE, DROP USER)T+14dHard delete
8Working tree (output-projects/{slug}) — delete immediate (žádný grace)immediateSource 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í

AlternativaDů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 periodKomplikuje 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ěří

ResourceMetricZdrojFrekvenceGranularita
DB storagepg_database_size(datname)Postgres pg_cataloghourly snapshotper-DB → per-app
NFS storagedu -sb /exports/user_{userId}/{slug}NFS server pod crondaily snapshotper-subdir → per-app
Compute (CPU+RAM)container_cpu_usage_seconds_total, container_memory_working_set_bytesK8s metrics-server / Prometheusscrape 30s, aggregate hourlyper-namespace → per-app
Pod active hourskube_pod_status_phase{phase="Running"}Prometheusscrape 30s, aggregate hourlyper-namespace → per-app
DB connectionspg_stat_activity count by usenamePostgres statscrape 60sper-role → per-app
DB query timepg_stat_database.{tup_returned, blks_read, blks_hit}Postgres statscrape 60s, delta hourlyper-DB → per-app
Network egressNetworkPolicy + Prometheus (container_network_transmit_bytes_total)K8sscrape 30s, aggregate hourlyper-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.
TierPricing modelInclusiveOverage
Free$0 / měshard quotas (viz §5)žádný overage; přes limit = app vypnutá
Starterflat $X / městier kvótyžádný overage v MVP (alert na 100% → upgrade prompt)
Proflat $Y / měs + meteredtier kvóty$/GB-měsíc storage, $/pod-hour, $/GB egress
Enterprisesmluvní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:

  1. Hourly aggregator (TalkIDE platform cron) čte raw metriky z Prometheus / Postgres / NFS cron a zapisuje do usage_records tabulky:

    usage_records: id, project_id, user_id, period_start, period_end, metric_type,
                   value, unit
    

    metric_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{}.

  2. Daily roll-up: aggregátor sumarizuje usage_records do usage_daily (jeden řádek per project per day per metric_type). Účel: rychlé dashboardy, billing performance.

  3. Monthly billing run: 1. den měsíce TalkIDE platform spočítá z usage_daily celkovou spotřebu za předchozí měsíc, porovná s tier inclusive bundle, vygeneruje invoice s overage položkami.

  4. 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í

AlternativaDů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 billingVelmi 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šechnyEnterprise 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

LimitFreeStarterProEnterprise
Max projektů (apps)1310smluvní
Max DB size per app100 MB1 GB10 GBsmluvní
Max NFS storage per app500 MB5 GB50 GBsmluvní
Max pod-hours / měs (active runtime, summed across apps)30 h (~1 h/day)200 h720 h (= 24×30, “always-on”)smluvní
Max concurrent DB connections per app51025smluvní
Max bandwidth egress / měs1 GB50 GB500 GBsmluvní
Autohibernate idle timeout5 min15 min30 minkonfigurovatelné
Cold start tolerance5-10 s (acceptable)5-10 s5-10 soption to disable hibernace
Custom domainNEANO (1 per app)ANO (5 per app)unlimited
Backup retentionžádný7 dní daily30 dní daily + weeklykonfigurovatelné
Supportcommunityemail 48hemail 12h + chatdedicated 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: quota system call NEBO daily du check + read-only mount.
  • Max projektů: TalkIDE BE refusne Create project request.
  • 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í

AlternativaDůvod zamítnutí
Per-app pod-hours limitPenalizuje 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 tierFree tier je kritický pro acquisition; non-technical users chtějí “yzkouset” před commitnutím. Hard quotas drží free tier ekonomicky únosný.
Unlimited storageStorage 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:

AlternativaDůvod zamítnutí
(b) Shared DB + per-app schemaCross-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 clusterMinimální cluster = $15/měs (DO Basic). Pro free tier je to economically nemožné. Operations overhead × N.
Per-app dedicated clusterNá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: subPath mount + 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í:

AlternativaDůvod zamítnutí
Dedicated PVC per appKaž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 storageUser 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 appRWO-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_records tabulka → 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í:

AlternativaDůvod zamítnutí
Žádná hibernace, always-onCompute cost roste lineárně s počtem apps; ekonomicky neudržitelné na Free tier.
Hibernace včetně DBDB hibernace u managed Postgres není feature; DROP+RESTORE per hibernace cyklus by zničil performance a data.
Knative ServingMature, 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 \l listu.

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

  1. Custom domains (Pro+ tier): user chce myapp.com místo myapp.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.
  2. 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.
  3. 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).
  4. 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_dump cron? PITR je clusterové, nelze restore jen jedné user-app DB bez ovlivnění ostatních.
  5. 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.
  6. 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.
  7. 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

  1. 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.
  2. 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).
  3. 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.
  4. 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.
  5. 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

  1. 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.
  2. Audit log per user-app resource changes: kdo kdy provedl deploy / delete / quota change? Pro Enterprise compliance. Žije v platform DB (audit_events tabulka), ale spec to neřeší.
  3. Disaster recovery testing: jak často testujeme restore z backupu? RPO/RTO targets per tier. Out-of-scope MVP.
  4. 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.)


Was this page helpful?

Thanks for the feedback.