Built-in object storage SDK pro user appky generované TalkIDE platformou. Každý projekt dostane vlastní Cloudflare R2 bucket s per-tenant scoped tokenem a in-namespace storage gateway (Spring Boot mikroservis), který Mara/scaffold automaticky nasadí — user appka nikdy nevidí cloud credentials.
Overview
TalkIDE poskytuje user appkám (scaffold-generated Kotlin Spring Boot) hotový storage SDK
(talkide-storage-sdk) + per-namespace storage gateway (talkide-storage-svc).
Storage gateway překládá vysokoúrovňové operace user appky (upload, download, list, delete)
na Cloudflare R2 (S3-compatible API) volání pomocí scoped per-bucket tokenu.
Architecture summary:
User Browser
│ presigned PUT/GET (direct data path)
▼
┌──────────────────────────────────────────────┐
│ Cloudflare R2 (per-project bucket) │
│ talkide-app-{tenantSlug}-{projectSlug} │
└──────────────────────────────────────────────┘
▲
│ S3 SDK (scoped token z K8s Secret)
│
┌──────────────────────────────────────────────┐
│ Namespace: {tenant-slug}-{env-slug} │
│ │
│ ┌────────────┐ in-ns HTTP ┌────────┐ │
│ │ user-app │ ───────────────► │storage │ │
│ │ (Spring) │ TALKIDE_APP_ │ -svc │ │
│ │ │ STORAGE_TOKEN │(Spring)│ │
│ └────────────┘ └────────┘ │
└──────────────────────────────────────────────┘
▲
│ provisioning at Create Project
│
┌──────────────────────────────────────────────┐
│ Platform talkide-be (control-plane) │
│ K8sStorageProvisioner │
│ 1. Cloudflare API → create bucket │
│ 2. Cloudflare API → create scoped token │
│ 3. K8s Secret create v tenant-env ns │
│ 4. Helm template apply storage-svc │
│ 5. zápis do app_storage_config (cluster A)│
└──────────────────────────────────────────────┘
Klíčové principy:
- Storage provider: Cloudflare R2 (S3-compatible API, free egress, soft cap 1000 buckets/account).
- Per-tenant bucket: 1 bucket per projekt, naming
talkide-app-{tenantSlug}-{projectSlug}(RFC 1123 compliant — viz UC-12001). - Per-tenant API token: Cloudflare scoped token (read+write pouze na svůj bucket), uložený v K8s Secret
storage-credsv tenant-env namespace. - Per-namespace storage gateway: Spring Boot mikroservis (
talkide-storage-svc) deploynutý v každém tenant-env namespace. Pattern parity s ADR-024 worker. - User app ↔ storage-svc autentikace: shared secret
TALKIDE_APP_STORAGE_TOKEN(HMAC, in-namespace only, K8s NetworkPolicy zabraňuje cross-ns). - Platform credentials nikdy v user app podu: všechny R2 credentials drží buď platform talkide-be (master Cloudflare API token) nebo storage-svc (per-bucket scoped token). User app vidí jen storage-svc URL + HMAC token.
- Data tok: přímý browser ↔ R2 přes presigned URL; storage-svc i platform BE se neúčastní samotného přenosu bajtů.
- Architecture parity: viz ADR-027 (Per-namespace Storage Gateway), který formalizuje pattern shodný s ADR-024 (worker per ns).
DB Schema changes (cluster A — control-plane)
Nová tabulka v platform control-plane PG. Production phase = forward-only migration, nový changeset file (číslo doplnit při implementaci — aktuálně cca 0053, vždy ověř podle talkide-be/src/main/resources/db/changelog/changes/).
| Migration file | Change |
|---|---|
XXXX-create-app-storage-config.xml | CREATE TABLE app_storage_config (viz UC-12001 sekce DB Schema) |
UC Index
| UC ID | Name | Status | Description |
|---|---|---|---|
| UC-12001 | Provision Storage | Planned | Bootstrap per-app storage při Create Project: Cloudflare API create bucket + scoped token + K8s Secret + Helm template apply storage-svc + zápis app_storage_config. |
| UC-12002 | Presign Upload URL | Planned | Storage-svc endpoint — user appka požádá o presigned PUT URL pro objekt; storage-svc validuje content-type/length/quota a vrátí URL s 15min TTL. |
| UC-12003 | Presign Download URL | Planned | Storage-svc endpoint — vrátí presigned GET URL s 60min TTL. |
| UC-12004 | List & Delete Files | Planned | Storage-svc BE-proxy endpointy — list objektů s prefix filterem + delete objektu. |
| UC-12005 | Storage SDK Scaffold | Planned | Kotlin lib talkide-storage-sdk přibalená do scaffoldu user appky + update Mara promptu v talkide-backend-dev.md (vrstva 2 dle CLAUDE.md). |
| UC-12006 | Storage Usage View | Planned | Per-project storage snapshot scheduler (30 min) → storage_usage_snapshot ledger + admin read endpoint. Forward-only growth ledger; primary billing-vstup pro per-project storage line item (následný UC-10019 v UC-10 Stripe Billing). |
Open Decisions (LOCKED for MVP)
| ID | Otázka | Rozhodnutí |
|---|---|---|
| OD-1 | Quota | Default quota_bytes = NULL (= unlimited). Primary mechanism = snapshot + billing (UC-12006) — tenanti se účtují podle skutečně obsazeného místa (parity s existing DO Spaces / NFS billing modelem). Hard limit zůstává jako volitelná bezpečnostní pojistka: admin nastaví app_storage_config.quota_bytes > 0 per projekt → storage-svc začne odmítat presign-upload s 429 při overshoot. NULL cesta (default) přeskočí quota check úplně, žádný overhead, žádné zaškrcení byznysu. |
| OD-2 | Public vs signed URLs | Pouze signed URLs v MVP. R2 podporuje public buckets, ale to by zničilo per-bucket isolation. Public ACL = follow-up issue. |
| OD-3 | Multipart upload | Single PUT ≤ 100 MB v MVP. Multipart later (R2 API podporuje, ale komplikuje SDK). |
| OD-4 | Content-type whitelist | Žádný platform allow-list. User app si MIME reguluje sama. Storage-svc validuje pouze, že Content-Type header je přítomný. |
| OD-5 | User app ↔ storage-svc auth | Shared HMAC secret (TALKIDE_APP_STORAGE_TOKEN env var v obou podech) + K8s NetworkPolicy. Žádné mTLS/JWT v MVP. |
| OD-6 | Bucket naming pattern | talkide-app-{tenantSlug}-{projectSlug}. Max 63 znaků (R2 limit), RFC 1123 (lowercase, alfanum, hyphen). Pokud součet > 63, BE truncate projectSlug a appendne 6-char hash. Reserved slugy z CLAUDE.md zůstávají v platnosti. |
| OD-7 | Token lifetime | Perpetual scoped tokens. Rotace mimo MVP (manuální runbook + Cloudflare API support). Revocation při project delete (viz UC-12001 sekce Lifecycle). |
| OD-8 | Storage-svc resource limits | Spring Boot mikroservis, baseline: requests: 256Mi/250m, limits: 512Mi/500m. JVM heap -Xmx384m. Replicas: 1 v MVP (žádné HA), restart policy Always. |
| OD-9 | Snapshot frequency | 30 min (parity s existing usage tracking — viz CLAUDE.md user feedback “snapshoty po 30m”). Platform talkide-be scheduler (@Scheduled(fixedDelay = 1_800_000)) iteruje aktivní projekty a volá storage-svc /internal/usage. Hodnota konfigurovatelná v application-production.yaml (talkide.storage.snapshot.interval-ms). |
| OD-10 | Usage source-of-truth | Storage-svc agregát přes ListObjectsV2 (paginated SUM Contents[].Size). Cloudflare R2 má v Dashboard API metriku r2_storage_metrics, ale není real-time (zpoždění až několik hodin) a nesedí na per-bucket granularitu konzistentně. ListObjectsV2 sum je deterministický a vždy aktuální (eventually-consistent v rámci sekund po PUT/DELETE). Cena: extra Class B operace, ale 30min frequency × 1000 projektů × průměrně 2 stránky listu = ~2880 ops/hod ≈ zanedbatelné. |
Security Model
| Aspekt | Řešení |
|---|---|
| Master Cloudflare API token | Drží výhradně platform talkide-be (env var CLOUDFLARE_API_TOKEN, K8s Secret cloudflare-creds v talkide ns). |
| Per-bucket scoped token | Vytvořen platformou per projekt; uložen v K8s Secret storage-creds v tenant-env ns; čtený výhradně storage-svc podem. |
| User app HMAC token | TALKIDE_APP_STORAGE_TOKEN env var v obou podech (user-app + storage-svc); 32B random hex generovaný platformou při provisioningu. |
| Platform internal token | PLATFORM_INTERNAL_TOKEN — separátní 32B random hex per storage-svc instance, generovaný platformou při provisioningu, uložený v K8s Secret storage-creds (key PLATFORM_INTERNAL_TOKEN). Používá ho výhradně platform talkide-be scheduler (UC-12006) pro volání /internal/usage endpointu na storage-svc. NIKDY se nedostane do user-app podu. K8s NetworkPolicy povoluje volání na /internal/* jen z pod-selectoru app.kubernetes.io/component=platform-scheduler v talkide namespace. |
| Cross-namespace isolation | K8s NetworkPolicy: storage-svc accepts user-API traffic (/api/v1/storage/*) POUZE z user-app podu v same ns; /internal/* POUZE z platform scheduler podu v talkide ns. Deny all from other ns. |
| Bucket isolation | Cloudflare scoped token má read+write pouze na svůj bucket → i kdyby útočník token získal, nedostane se mimo svůj projekt. |
| Path traversal | Storage-svc sanitizuje key: odmítá .., absolutní cesty, double slash, řídicí znaky, NUL bytes. Max key length 1024 znaků (R2 limit). |
| Presigned URL TTL | Upload: 15 min; Download: 60 min. Nelze override z user app strany. |
| Quota enforcement | Default quota_bytes = NULL → žádný hard limit, žádný check (unlimited path). Pokud admin nastaví quota_bytes > 0, storage-svc dělá best-effort check při presign-upload (cached 60s, periodic refresh z platform). Authoritative usage drží storage_usage_snapshot ledger (UC-12006), nikoli živý čítač. |
| Logy | Storage-svc loguje key + size + operation, NIKDY samotný content. Master token, scoped token, HMAC token i internal token NIKDY do logu (Logbook filter rule). |
Lifecycle: Project deletion
Když je projekt smazán (UC-03007), platform talkide-be musí v K8sStorageProvisioner.deprovision():
- Smazat všechny objekty v bucketu (R2 batch delete, max 1000 keys/request, opakovat dokud bucket není prázdný).
- Smazat bucket (R2 DELETE bucket).
- Revokovat scoped token (Cloudflare API).
- Smazat K8s Secret
storage-credsv tenant-env ns. - Smazat storage-svc Deployment + Service + NetworkPolicy (helm template uninstall).
- Smazat řádek
app_storage_config.
Failures v krocích 1–5 jsou logované a alertované, ale neblokují DB cascade (consistent s pattern v K8sWorkerProvisioner.deprovision — best-effort cleanup, manuální runbook při drift). Viz be#120 (tenant deprovision lifecycle).
References
- ADR-027: Per-namespace Storage Gateway (pattern parity s ADR-024). Soubor:
adr/ADR-027-per-namespace-storage-gateway.md. - ADR-023: Shared dataplane DB schema-per-app (analogická izolační filozofie).
- ADR-024: Workspace runtime worker extraction (pattern parity pro per-ns mikroservis topology + samostatné repo pro per-ns komponentu).
- GitLab repo
talkide/talkide-storage-svc: samostatný git repo s Gradle multi-module setupem — modul:service(Spring Boot mikroservis → imageregistry.digitalocean.com/talkide/talkide-storage-svc:{tag}) + modul:sdk(Kotlin libapp.talkide:talkide-storage-sdkpublishovaný do GitLab Package Registry). Viz ADR-027 sekce “talkide-storage-svc” a UC-12005 sekce “SDK source structure”. - GitLab repo
talkide/talkide-infra: Helm chartcharts/storage-svc/aplikovanýK8sStorageProvisionerem (deploy manifesty NEžijí vtalkide-storage-svcrepu). - CLAUDE.md sekce “Storage strategy” — overall TalkIDE storage design (pozn.: před tímto UC zmiňovala DO Spaces; po pivotu na R2 bude updated v rámci implementace).
- GitLab tracking: be#150.
Thanks for the feedback.