Internal Documentation internal
TalkIDE internal documentation

Při vytvoření projektu (UC-03002) platform talkide-be automaticky provisionuje per-project storage: Cloudflare R2 bucket + scoped API token + K8s Secret v tenant-env namespace + storage-svc Deployment/Service/NetworkPolicy + řádek v app_storage_config. Volání tohoto UC NENÍ samostatný HTTP endpoint — je to interní orchestrace v rámci Create Project use case.

  • Pattern parity s K8sWorkerProvisioner (ADR-024) a K8sEnvironmentProvisioner (UC-10013); formalizováno v ADR-027 (Per-namespace Storage Gateway).
  • Provisioner implementuje rozhraní StorageProvisioner se dvěma beanami:
    • CloudflareR2StorageProvisioner — produkční (@Profile("production"))
    • NoopStorageProvisioner — lokální/test (@Profile("!production"))
  • Per-namespace topologie: jedna instance storage-svc per {tenant-slug}-{env-slug} namespace per projekt. Tj. namespace může hostit více storage-svc Deploymentů (jeden per projekt v tom namespace), Service jmenování storage-svc-{projectSlug}.
  • Idempotence: re-volání pro existující projekt nevytváří nový bucket — provisioner detekuje existující app_storage_config row a ověří, že K8s resources žijí; chybějící patch-vytvoří.

Provisioner flow

sequenceDiagram
    actor CreateProjectUC as Create Project UseCase
    participant SP as K8sStorageProvisioner
    participant CF as Cloudflare API
    participant K8s as K8s API
    participant DB as Cluster A (control-plane PG)

    CreateProjectUC->>+SP: provision(tenantId, tenantSlug, projectId, projectSlug, envSlug)

    SP->>SP: derive bucketName = "talkide-app-{tenantSlug}-{projectSlug}" <br> (truncate + hash if > 63 chars)

    SP->>DB: SELECT * FROM app_storage_config WHERE project_id = ?
    alt row already exists
        SP->>K8s: verify Secret + Deployment + Service + NetworkPolicy exist
        alt all live
            SP-->>CreateProjectUC: return existing AppStorageConfig (idempotent no-op)
        else any K8s resource missing
            SP->>K8s: re-apply Helm template (patch)
            SP-->>CreateProjectUC: return existing AppStorageConfig
        end
    end

    SP->>+CF: POST /accounts/{accountId}/r2/buckets <br> { name: bucketName, locationHint: "weur" }
    alt bucket creation fails (name collision, quota)
        CF-->>SP: 4xx error
        SP-->>CreateProjectUC: throw StorageProvisioningException
    end
    CF->>-SP: 200 OK <br> { bucket: { name, creation_date } }

    SP->>+CF: POST /accounts/{accountId}/tokens <br> { name, policies: [{ effect: "allow", permission_groups: ["r2-object-read-write"], resources: { bucket: bucketName } }] }
    CF->>-SP: 200 OK <br> { token: { id, value }, accessKeyId, secretAccessKey }

    SP->>SP: generate appStorageToken = secureRandomHex(32) <br> generate platformInternalToken = secureRandomHex(32)

    SP->>+K8s: CREATE Secret "storage-creds" in ns "{tenantSlug}-{envSlug}" <br> { CF_ACCESS_KEY_ID, CF_SECRET_ACCESS_KEY, CF_R2_BUCKET, CF_R2_ACCOUNT_ID, APP_STORAGE_TOKEN, PLATFORM_INTERNAL_TOKEN }
    K8s->>-SP: 201 Created

    SP->>+K8s: Helm template apply (Deployment + Service + NetworkPolicy) <br> name "storage-svc-{projectSlug}"
    K8s->>-SP: applied

    SP->>+DB: INSERT INTO app_storage_config (project_id, bucket_name, cf_token_id, app_storage_token_hash, platform_internal_token_hash, quota_bytes, bytes_used, created_at) <br> quota_bytes = NULL (unlimited default)
    DB->>-SP: row inserted

    SP->>-CreateProjectUC: return AppStorageConfig

DB Schema

Nový Liquibase changeset v talkide-be/src/main/resources/db/changelog/changes/ — produkční fáze (immutable, vždy nový soubor s dalším volným číslem, viz CLAUDE.md). Při psaní tohoto UC je další volný slot pravděpodobně 0053 (před implementací vždy ověř podle aktuálního stavu adresáře).

Tabulka app_storage_config (cluster A — control-plane):

SloupecTypConstraintsPoznámka
idBIGSERIALPKAuto-generated
project_idBIGINTNOT NULL, UNIQUE, FK → projects(id) ON DELETE CASCADEJeden storage config per projekt
bucket_nameVARCHAR(63)NOT NULL, UNIQUERFC 1123 compliant, max 63 znaků (R2 limit)
cf_account_idVARCHAR(64)NOT NULLCloudflare account ID (denormalizováno pro audit)
cf_token_idVARCHAR(64)NOT NULLCloudflare token ID (pro revocation při delete)
cf_access_key_idVARCHAR(128)NOT NULLR2 access key ID (uložené taky v K8s Secret — DB = source of truth pro reconciliation)
cf_secret_access_key_encTEXTNOT NULLR2 secret access key, šifrovaný platform-side AES-GCM klíčem z application-production.yaml
app_storage_token_hashVARCHAR(128)NOT NULLSHA-256 hash TALKIDE_APP_STORAGE_TOKEN (raw token jen v K8s Secret)
platform_internal_token_hashVARCHAR(128)NOT NULLSHA-256 hash PLATFORM_INTERNAL_TOKEN (raw token jen v K8s Secret); ověřuje volání platform scheduler → /internal/* na storage-svc (viz UC-12006)
quota_bytesBIGINTNULL (default)NULL = unlimited (no hard limit, no quota check). Pokud admin nastaví > 0, storage-svc začne v UC-12002 presign-upload odmítat při overshoot. Primary mechanism je snapshot+billing v UC-12006, ne enforcement.
bytes_usedBIGINTNOT NULL DEFAULT 0Best-effort live čítač; updatovaný storage-svc async callbackem po DELETE (UC-12004) a po PUT-confirm (UC-12002 best-effort path). NENÍ authoritative — authoritative usage drží storage_usage_snapshot ledger (UC-12006). Tento sloupec slouží jen pro fast quota check, když quota_bytes IS NOT NULL.
created_atTIMESTAMPTZNOT NULL DEFAULT NOW()
updated_atTIMESTAMPTZNOT NULL DEFAULT NOW()

Indexy:

  • PRIMARY KEY (id)
  • UNIQUE INDEX uk_app_storage_config_project_id ON app_storage_config(project_id)
  • UNIQUE INDEX uk_app_storage_config_bucket_name ON app_storage_config(bucket_name)

Liquibase XML skeleton:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                       http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.0.xsd">

    <changeSet id="0053-create-app-storage-config" author="talkide">
        <createTable tableName="app_storage_config">
            <column name="id" type="BIGSERIAL"><constraints primaryKey="true" nullable="false"/></column>
            <column name="project_id" type="BIGINT"><constraints nullable="false" unique="true"
                foreignKeyName="fk_app_storage_config_project"
                referencedTableName="projects" referencedColumnNames="id"/></column>
            <column name="bucket_name" type="VARCHAR(63)"><constraints nullable="false" unique="true"/></column>
            <column name="cf_account_id" type="VARCHAR(64)"><constraints nullable="false"/></column>
            <column name="cf_token_id" type="VARCHAR(64)"><constraints nullable="false"/></column>
            <column name="cf_access_key_id" type="VARCHAR(128)"><constraints nullable="false"/></column>
            <column name="cf_secret_access_key_enc" type="TEXT"><constraints nullable="false"/></column>
            <column name="app_storage_token_hash" type="VARCHAR(128)"><constraints nullable="false"/></column>
            <column name="platform_internal_token_hash" type="VARCHAR(128)"><constraints nullable="false"/></column>
            <column name="quota_bytes" type="BIGINT"><constraints nullable="true"/></column>
            <column name="bytes_used" type="BIGINT" defaultValueNumeric="0"><constraints nullable="false"/></column>
            <column name="created_at" type="TIMESTAMP WITH TIME ZONE" defaultValueComputed="NOW()"><constraints nullable="false"/></column>
            <column name="updated_at" type="TIMESTAMP WITH TIME ZONE" defaultValueComputed="NOW()"><constraints nullable="false"/></column>
        </createTable>
        <addForeignKeyConstraint baseTableName="app_storage_config" baseColumnNames="project_id"
                                 constraintName="fk_app_storage_config_project"
                                 referencedTableName="projects" referencedColumnNames="id"
                                 onDelete="CASCADE"/>
    </changeSet>

</databaseChangeLog>

Bucket naming algorithm

fun buildBucketName(tenantSlug: String, projectSlug: String): String {
    val prefix = "talkide-app-$tenantSlug-"
    val maxProjectSlugLen = 63 - prefix.length
    if (projectSlug.length <= maxProjectSlugLen) {
        return "$prefix$projectSlug"
    }
    // truncate + 6-char SHA-256 hash suffix pro deterministic uniqueness
    val hash = sha256("$tenantSlug-$projectSlug").take(6).lowercase()
    val truncated = projectSlug.take(maxProjectSlugLen - 7)  // -7 = "-" + 6 hash chars
    return "$prefix$truncated-$hash"
}

Příklady:

  • tenantSlug=popelkam, projectSlug=todo-listtalkide-app-popelkam-todo-list (28 znaků, OK)
  • tenantSlug=acmecorporation, projectSlug=very-long-project-slug-that-goes-over-the-limit → truncated + hash → talkide-app-acmecorporation-very-long-project-slug--a3f8b2 (63 znaků)

Validace v BE před voláním provisioneru:

  • tenantSlug a projectSlug jsou už validované na úrovni Create Tenant / Create Project (RFC 1123, reserved slug check).
  • Výsledný bucketName musí matchovat regex ^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$ (R2 requirement).

K8s resources (Helm template)

Storage-svc deploy do {tenantSlug}-{envSlug} namespace. Template lives v talkide-infra/charts/storage-svc/.

Deployment storage-svc-{projectSlug}:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: storage-svc-{{ .Values.projectSlug }}
  namespace: {{ .Values.tenantSlug }}-{{ .Values.envSlug }}
  labels:
    app.kubernetes.io/name: storage-svc
    app.kubernetes.io/instance: {{ .Values.projectSlug }}
    talkide.app/tenant: {{ .Values.tenantSlug }}
    talkide.app/project: {{ .Values.projectSlug }}
spec:
  replicas: 1
  strategy: { type: Recreate }   # parita s K8sWorkerProvisioner (#275)
  selector:
    matchLabels:
      app.kubernetes.io/name: storage-svc
      app.kubernetes.io/instance: {{ .Values.projectSlug }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: storage-svc
        app.kubernetes.io/instance: {{ .Values.projectSlug }}
    spec:
      containers:
        - name: storage-svc
          image: registry.digitalocean.com/talkide/talkide-storage-svc:{{ .Values.image.tag }}
          imagePullPolicy: IfNotPresent
          ports:
            - { name: http, containerPort: 8080 }
          env:
            - { name: SPRING_PROFILES_ACTIVE, value: "production" }
            - { name: JAVA_OPTS, value: "-Xmx384m -XX:+UseG1GC" }
            - name: CF_R2_BUCKET
              valueFrom: { secretKeyRef: { name: storage-creds, key: CF_R2_BUCKET } }
            - name: CF_R2_ACCOUNT_ID
              valueFrom: { secretKeyRef: { name: storage-creds, key: CF_R2_ACCOUNT_ID } }
            - name: CF_ACCESS_KEY_ID
              valueFrom: { secretKeyRef: { name: storage-creds, key: CF_ACCESS_KEY_ID } }
            - name: CF_SECRET_ACCESS_KEY
              valueFrom: { secretKeyRef: { name: storage-creds, key: CF_SECRET_ACCESS_KEY } }
            - name: APP_STORAGE_TOKEN
              valueFrom: { secretKeyRef: { name: storage-creds, key: APP_STORAGE_TOKEN } }
            - name: PLATFORM_INTERNAL_TOKEN
              valueFrom: { secretKeyRef: { name: storage-creds, key: PLATFORM_INTERNAL_TOKEN } }
          resources:
            requests: { memory: 256Mi, cpu: 250m }
            limits:   { memory: 512Mi, cpu: 500m }
          livenessProbe:
            httpGet: { path: /actuator/health/liveness, port: http }
            initialDelaySeconds: 30
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: http }
            initialDelaySeconds: 5

Service storage-svc-{projectSlug}:

apiVersion: v1
kind: Service
metadata:
  name: storage-svc-{{ .Values.projectSlug }}
  namespace: {{ .Values.tenantSlug }}-{{ .Values.envSlug }}
spec:
  type: ClusterIP
  ports:
    - { name: http, port: 80, targetPort: http }
  selector:
    app.kubernetes.io/name: storage-svc
    app.kubernetes.io/instance: {{ .Values.projectSlug }}

NetworkPolicy — accept traffic POUZE z (a) user-app podu v same ns pro user API, (b) platform scheduler podu v talkide ns pro internal endpointy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: storage-svc-{{ .Values.projectSlug }}-ingress
  namespace: {{ .Values.tenantSlug }}-{{ .Values.envSlug }}
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: storage-svc
      app.kubernetes.io/instance: {{ .Values.projectSlug }}
  policyTypes: [Ingress]
  ingress:
    # (a) user-app v same ns → user-facing API
    - from:
        - podSelector:
            matchLabels:
              talkide.app/project: {{ .Values.projectSlug }}
              talkide.app/role: user-app
      ports:
        - { protocol: TCP, port: 8080 }
    # (b) platform talkide-be scheduler v ns "talkide" → /internal/* (snapshot UC-12006)
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: talkide
          podSelector:
            matchLabels:
              app.kubernetes.io/component: platform-scheduler
      ports:
        - { protocol: TCP, port: 8080 }

User-app Deployment musí mít labely talkide.app/project: {projectSlug} a talkide.app/role: user-app (Mara prompt update v UC-12005). Platform scheduler pod (talkide-be) musí mít label app.kubernetes.io/component: platform-scheduler na svém Deploymentu — viz UC-12006 sekce “Scheduler topology”.

Path-based routing v storage-svc doplňuje NetworkPolicy: Spring Security filter ověřuje, že požadavky na /internal/* mají Authorization: Bearer <PLATFORM_INTERNAL_TOKEN>, požadavky na /api/v1/storage/* mají X-Talkide-App-Token: <APP_STORAGE_TOKEN>. Cross-použití tokenů = 401.


K8s Secret storage-creds content

KeyValue (base64-encoded by K8s)
CF_R2_BUCKETbucket name (např. talkide-app-popelkam-todo-list)
CF_R2_ACCOUNT_IDCloudflare account ID
CF_ACCESS_KEY_IDR2 access key z scoped tokenu
CF_SECRET_ACCESS_KEYR2 secret access key z scoped tokenu
APP_STORAGE_TOKEN32B hex random — user-app ↔ storage-svc HMAC shared secret (chrání /api/v1/storage/*)
PLATFORM_INTERNAL_TOKEN32B hex random — platform talkide-be ↔ storage-svc service-to-service bearer (chrání /internal/*, čte ho výhradně snapshot scheduler v UC-12006)

User-app Deployment musí mít namountovaný Secret jako env var (UC-12005 scaffold):

env:
  - name: TALKIDE_APP_STORAGE_TOKEN
    valueFrom: { secretKeyRef: { name: storage-creds, key: APP_STORAGE_TOKEN } }
  - name: TALKIDE_APP_STORAGE_URL
    value: "http://storage-svc-{{ projectSlug }}.{{ tenantSlug }}-{{ envSlug }}.svc.cluster.local"

Cloudflare API contract (provisioner side)

1. Create R2 bucket:

POST https://api.cloudflare.com/client/v4/accounts/{accountId}/r2/buckets
Authorization: Bearer {CLOUDFLARE_API_TOKEN}
Content-Type: application/json

{
  "name": "talkide-app-popelkam-todo-list",
  "locationHint": "weur"
}

2. Create scoped R2 token:

POST https://api.cloudflare.com/client/v4/accounts/{accountId}/tokens
Authorization: Bearer {CLOUDFLARE_API_TOKEN}
Content-Type: application/json

{
  "name": "r2-talkide-app-popelkam-todo-list",
  "policies": [{
    "effect": "allow",
    "permission_groups": [
      { "id": "<r2-object-read-write-group-id>" }
    ],
    "resources": {
      "com.cloudflare.api.account.r2.bucket.{accountId}/talkide-app-popelkam-todo-list": "*"
    }
  }]
}

Response obsahuje result.value (Cloudflare API token), result.access_key_id, result.secret_access_key (S3 credentials pro scoped bucket access).

3. Master Cloudflare API token (platform-level):

  • Permissions: Account → Workers R2 Storage → Edit + User API Tokens → Edit.
  • Uložen v K8s Secret cloudflare-creds v talkide ns, key api-token.
  • Vyzískat: kubectl get secret -n talkide cloudflare-creds -o jsonpath='{.data.api-token}' | base64 -d.

Internal contract (talkide-be Kotlin)

package com.mddsummer.talkide.features.storage.infrastructure

interface StorageProvisioner {
    fun provision(input: StorageProvisionInput): AppStorageConfig
    fun deprovision(projectId: Long)
}

data class StorageProvisionInput(
    val tenantId: Long,
    val tenantSlug: String,
    val projectId: Long,
    val projectSlug: String,
    val envSlug: String,
    val quotaBytes: Long? = null,  // NULL = unlimited (default); admin override-only
)

data class AppStorageConfig(
    val id: Long,
    val projectId: Long,
    val bucketName: String,
    val storageSvcUrl: String,    // http://storage-svc-{slug}.{ns}.svc.cluster.local
    val quotaBytes: Long?,        // NULL = unlimited
    val bytesUsed: Long,           // best-effort live counter; authoritative usage v storage_usage_snapshot (UC-12006)
)

@Profile("production")
@Component
class CloudflareR2StorageProvisioner(...) : StorageProvisioner { ... }

@Profile("!production")
@Component
class NoopStorageProvisioner(...) : StorageProvisioner {
    override fun provision(input: StorageProvisionInput): AppStorageConfig {
        // pouze zápis do DB; bucket = "noop-{projectId}", svcUrl = "http://localhost:0"
    }
    override fun deprovision(projectId: Long) { /* DB delete only */ }
}

Volání z CreateProjectUseCase:

val storageConfig = storageProvisioner.provision(
    StorageProvisionInput(tenant.id, tenant.slug, project.id, project.slug, environment.slug)
)
// storageConfig.storageSvcUrl se předá do K8sAppDeployer pro user-app env var

Pozn. ke Spring profilu: Production profil je production (NE prod) — viz CLAUDE.md a incident #194. Všechny @Profile anotace musí používat correct název.


Error handling

ScénářChování
Cloudflare API timeout / 5xxRetry s exponential backoff (3 pokusy, 1s/2s/4s). Po 3 selháních throw StorageProvisioningException; Create Project rollback (transactional).
Bucket name collision (DB UNIQUE violation nebo Cloudflare 409)StorageProvisioningException; FE zobrazí “Storage initialization failed, contact support”. Možný recovery: manuální admin runbook.
K8s Secret create failsRetry 3×; pak rollback (delete bucket + revoke token + throw).
Helm template apply failsStejně jako K8s Secret create.
DB INSERT fails (po úspěšném Cloudflare + K8s)KRITICKÝ DRIFT — log ERROR, alert PagerDuty, manuální cleanup runbook. Pravděpodobnost minimální (BD INSERT je poslední krok, DB connection failure tu nebývá).

Provisioner orchestruje rollback v opačném pořadí (LIFO): DB → K8s Helm uninstall → K8s Secret delete → Cloudflare token revoke → Cloudflare bucket delete.


Idempotence

Re-volání provision() pro existující project_id:

  1. SELECT FROM app_storage_config WHERE project_id = ?
  2. Pokud row neexistuje → full flow (jako první volání).
  3. Pokud row existuje:
    • Verify K8s Secret storage-creds exists in ns.
    • Verify Deployment storage-svc-{slug} exists in ns.
    • Verify Service storage-svc-{slug} exists in ns.
    • Verify NetworkPolicy exists in ns.
    • Verify Cloudflare bucket exists (HEAD bucket via S3 API).
    • Chybějící resources patch-apply (helm upgrade).
    • Nikdy nevytvářet nový Cloudflare token ani bucket (perpetual lifecycle, OD-7).
    • Return existing AppStorageConfig.

Test Cases

GIVENWHENTHEN
nový projekt, žádný existující app_storage_configprovisioner.provision() je volánCloudflare bucket vytvořen; scoped token vytvořen; K8s Secret + Deployment + Service + NetworkPolicy v ns vytvořeny; řádek app_storage_config zapsán; vrácen AppStorageConfig s storageSvcUrl
tenantSlug=popelkam, projectSlug=todo-listbuildBucketName()vrátí talkide-app-popelkam-todo-list (28 znaků)
tenantSlug=acmecorporation, projectSlug=very-long-project-slug-over-limit (kombinovaná délka > 63)buildBucketName()vrátí název ≤ 63 znaků, končící -{6-char-hash}
dva projekty stejného tenanta se stejným pre-truncate prefixembuildBucketName() pro obahashe se liší (deterministic, ale na různých project slugs) — žádná kolize
app_storage_config existuje, všechny K8s resources žijíprovisioner.provision() je volán (idempotent re-call)žádné Cloudflare volání; žádné K8s create; vrácen existující AppStorageConfig
app_storage_config existuje, Deployment chybí (např. manuálně smazán)provisioner.provision() je volánhelm template re-apply; bucket+token nedotčeny; vrácen existující AppStorageConfig
Cloudflare API vrátí 5xx 3× po soběprovisioner.provision() je volánthrow StorageProvisioningException; Create Project transaction rollback; žádný řádek app_storage_config
Cloudflare bucket vytvořen, ale K8s Secret create selžeprovisioner.provision() je volánrollback: revoke Cloudflare token + delete bucket; throw StorageProvisioningException
DB INSERT selže po úspěšném Cloudflare + K8s setupprovisioner.provision() je volánlog ERROR, alert; ruční cleanup runbook; StorageProvisioningException propaguje
projekt smazán (UC-03007)provisioner.deprovision(projectId) je volánbucket emptied (batch delete) + bucket deleted + token revoked + K8s Secret/Deployment/Service/NetworkPolicy smazány + app_storage_config row smazán (CASCADE)
deprovision: bucket batch-delete vrátí 5xxdeprovision() je volánretry 3×; pokud stále selhává, log ERROR + pokračovat dalšími kroky (best-effort, parita s K8sWorkerProvisioner.deprovision)
Noop profile (!production)provisioner.provision() je volánžádné Cloudflare/K8s volání; app_storage_config row vytvořen s bucket_name="noop-{projectId}", storage_svc_url="http://localhost:0"
invalid characters v projectSlug (např. uppercase)buildBucketName()throw IllegalArgumentException — slug validation je úkol Create Project UC, sem už nesmí dorazit invalid
bucket name kolize v Cloudflare (409)provisioner.provision() je volánlog WARN, retry 3× — pokud stále kolize, throw StorageProvisioningException (může indikovat orphaned bucket z předchozího failed provisioningu)
default volání bez explicit quotaBytesprovisioner.provision() je volánapp_storage_config.quota_bytes IS NULL (unlimited); UC-12002 quota check je full-skip path
admin volání s quotaBytes = 10 * 1024 * 1024 * 1024 (10 GB)provisioner.provision() je volánapp_storage_config.quota_bytes = 10737418240; UC-12002 quota check je aktivní
K8s Secret content auditpo úspěšném provisioninguSecret obsahuje VŠECH 6 keys: CF_R2_BUCKET, CF_R2_ACCOUNT_ID, CF_ACCESS_KEY_ID, CF_SECRET_ACCESS_KEY, APP_STORAGE_TOKEN, PLATFORM_INTERNAL_TOKEN
DB row audit po úspěšném provisioninguSELECT z app_storage_configplatform_internal_token_hash = SHA-256(PLATFORM_INTERNAL_TOKEN); app_storage_token_hash = SHA-256(APP_STORAGE_TOKEN); oba hashe se liší (různé tokeny)

Out of scope (this UC)

  • HTTP endpoint pro re-provisioning (admin-only) — manuální runbook v MVP.
  • Multi-region buckets — locationHint: "weur" je hardcoded; multi-region jako follow-up.
  • Quota change endpoint — app_storage_config.quota_bytes editovatelný jen přímou DB úpravou v MVP (default = NULL = unlimited; admin nasazuje hard limit ad-hoc).
  • Token rotation — perpetual v MVP (OD-7).
  • Storage usage snapshot scheduler + admin read endpoint — implementace v UC-12006 (status: Planned, součást MVP).

Was this page helpful?

Thanks for the feedback.