Internal Documentation internal
TalkIDE internal documentation

Kotlin lib talkide-storage-sdk přibalená do scaffoldu user appky + update Mara promptu v talkide-backend-dev.md (vrstva 2 dle CLAUDE.md) tak, aby Mara věděla, jak SDK použít a aby ho nezasahovala chybnými volánímy přes raw HTTP.

  • SDK je publikováno jako Gradle dependency v scaffold build.gradle.kts: implementation("app.talkide:talkide-storage-sdk:1.0.0").
  • SDK je thin wrapper nad storage-svc HTTP API (UC-12002, UC-12003, UC-12004).
  • Konfigurace přes Spring Boot auto-configuration — žádný code v Application.kt user appky.
  • Env vars TALKIDE_APP_STORAGE_URL a TALKIDE_APP_STORAGE_TOKEN injektované K8s Secret při deployi (provisionované UC-12001).

Scope of this UC

  1. Definovat veřejné API talkide-storage-sdk Kotlin lib.
  2. Definovat scaffold integraci — kde se přidá dependency, jaké env vars musí být v Deploymentu.
  3. Definovat Mara prompt update — co se přidá do talkide-backend-dev.md tak, aby Mara SDK znala.
  4. Definovat publish workflow — jak se SDK staví a publikuje.

Public API TalkideStorageClient

package app.talkide.storage

interface TalkideStorageClient {

    /** Request a presigned PUT URL. Throws [StorageException] on validation/auth/quota failures. */
    fun presignUpload(key: String, contentType: String, contentLength: Long): PresignedUpload

    /** Request a presigned GET URL. Throws [StorageException] on validation/auth failures. */
    fun presignDownload(key: String, contentDisposition: String? = null): PresignedDownload

    /** List objects under a prefix. Returns one page; call again with [PageResult.nextCursor] for next page. */
    fun list(prefix: String = "", cursor: String? = null, limit: Int = 100): PageResult<StorageObject>

    /** Delete an object by key. Idempotent — no error if the object did not exist. */
    fun delete(key: String)
}

data class PresignedUpload(val uploadUrl: String, val key: String, val expiresAt: Instant)
data class PresignedDownload(val downloadUrl: String, val key: String, val expiresAt: Instant)
data class StorageObject(val key: String, val sizeBytes: Long, val lastModified: Instant, val etag: String)
data class PageResult<T>(val items: List<T>, val nextCursor: String?, val hasMore: Boolean)

sealed class StorageException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) {
    class InvalidKey(message: String) : StorageException(message)
    class QuotaExceeded(message: String) : StorageException(message)
    class AuthenticationFailed(message: String) : StorageException(message)
    class UpstreamError(message: String, cause: Throwable? = null) : StorageException(message, cause)
}

Spring Boot auto-configuration

src/main/kotlin/app/talkide/storage/TalkideStorageAutoConfiguration.kt:

@AutoConfiguration
@ConditionalOnClass(TalkideStorageClient::class)
@EnableConfigurationProperties(TalkideStorageProperties::class)
class TalkideStorageAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    fun talkideStorageClient(props: TalkideStorageProperties): TalkideStorageClient =
        HttpTalkideStorageClient(props.url, props.token)
}

@ConfigurationProperties(prefix = "talkide.storage")
data class TalkideStorageProperties(
    val url: String,    // bind from TALKIDE_APP_STORAGE_URL via Spring relaxed binding
    val token: String,  // bind from TALKIDE_APP_STORAGE_TOKEN
)

src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:

app.talkide.storage.TalkideStorageAutoConfiguration

User appka pak jen:

@RestController
class AvatarController(private val storage: TalkideStorageClient) {

    @PostMapping("/api/avatars/upload-url")
    fun getUploadUrl(@RequestBody req: AvatarUploadRequest): PresignedUpload =
        storage.presignUpload(
            key = "uploads/avatars/${req.userId}.png",
            contentType = "image/png",
            contentLength = req.sizeBytes,
        )
}

Scaffold integration

build.gradle.kts (user-app BE)

Scaffold generator (scaffold-backend.sh resp. statická šablona talkide-be/templates/kotlin-spring/...) přidá:

dependencies {
    // ... existing ...
    implementation("app.talkide:talkide-storage-sdk:1.0.0")
}

repositories {
    mavenCentral()
    maven { url = uri("https://repo.talkide.app/maven") }  // private repo pro talkide-storage-sdk
}

application.yaml (user-app BE)

talkide:
  storage:
    url: ${TALKIDE_APP_STORAGE_URL}
    token: ${TALKIDE_APP_STORAGE_TOKEN}

K8s Deployment env (template generated by K8sAppDeployer v platform talkide-be)

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

Pod labels (required for storage-svc NetworkPolicy)

User-app Deployment musí mít labely:

metadata:
  labels:
    talkide.app/project: "{projectSlug}"
    talkide.app/role: "user-app"

Bez těchto labelů NetworkPolicy v UC-12001 zablokuje volání user-app → storage-svc.


Mara prompt update (talkide-be/plugin/agents/talkide-backend-dev.md)

CLAUDE.md vrstva 2 — AI-generovaný kód za běhu píše Theo (BE dev agent). Mara/Theo musí vědět:

Sekce k přidání do talkide-backend-dev.md:

## Storage SDK

Tvoje user appka má vždy k dispozici `TalkideStorageClient` Spring bean (z dependency `app.talkide:talkide-storage-sdk`).

**Použij ho VŽDYCKY** pro práci se soubory v user appce. NIKDY:
- Nepiš vlastní AWS S3 / Cloudflare R2 SDK kód.
- Nevolal `RestTemplate` / `WebClient` přímo proti `TALKIDE_APP_STORAGE_URL`.
- Neukládaj soubory na disk podu (pod má pouze ephemeral storage, restart = ztráta dat).
- Neukládej `TALKIDE_APP_STORAGE_TOKEN` do DB ani conversation history.

**Standardní pattern (upload):**
\`\`\`kotlin
@RestController
class FileController(private val storage: TalkideStorageClient) {
    @PostMapping("/api/files/upload-url")
    fun uploadUrl(@RequestBody req: UploadUrlRequest): PresignedUpload {
        // klient pak udělá PUT přímo na uploadUrl s contentType a binary body
        return storage.presignUpload(req.key, req.contentType, req.contentLength)
    }
}
\`\`\`

**Standardní pattern (download):**
\`\`\`kotlin
@GetMapping("/api/files/{key}/download-url")
fun downloadUrl(@PathVariable key: String): PresignedDownload =
    storage.presignDownload(key)
\`\`\`

**Standardní pattern (list):**
\`\`\`kotlin
@GetMapping("/api/files")
fun list(@RequestParam(required = false) prefix: String?,
         @RequestParam(required = false) cursor: String?): PageResult<StorageObject> =
    storage.list(prefix ?: "", cursor)
\`\`\`

**Key naming convention** (doporučení pro user appku):
- `uploads/{kategorie}/{uuid}.{ext}` — user-uploaded soubory
- `generated/{kategorie}/{date}/{name}` — AI generated nebo computed content
- NIKDY `..`, leading `/`, ani double `//` (SDK to odmítne s `StorageException.InvalidKey`).

**Error handling:**
\`\`\`kotlin
try {
    storage.presignUpload(key, ct, len)
} catch (e: StorageException.QuotaExceeded) {
    throw ResponseStatusException(HttpStatus.PAYMENT_REQUIRED, "Storage quota exceeded")
} catch (e: StorageException.InvalidKey) {
    throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message ?: "Invalid key")
}
\`\`\`

Pozn. ke CLAUDE.md vrstva 2: Bug v Maře / Theovi co jí v tomhle zabrání ⇒ oprava v promptu talkide-be/plugin/agents/talkide-backend-dev.md, ne ve scaffold šabloně. Prompt-as-code je bundlovaný v BE runtime → projeví se až po BE redeployi.


SDK source structure

Lokace: SDK lib žije jako Gradle modul :sdk v samostatném git repo talkide/talkide-storage-svc (Gradle multi-module setup vedle modulu :service pro Spring Boot mikroservis). Důvod společného repa: verzová konzistence DTO contractu mezi service a SDK (viz ADR-027 sekce “talkide-storage-sdk”). Alternativa — SDK v talkide-be/templates/... — byla odmítnuta, viz ADR-027 Alternatives.

Repo layout:

talkide-storage-svc/                       (git repo talkide/talkide-storage-svc)
├── settings.gradle.kts                    (include(":service", ":sdk"))
├── build.gradle.kts                       (root: shared Kotlin + Spring Boot BOM)
├── .gitlab-ci.yml                         (manual cost gate; service image + SDK publish jobs)
├── service/                               (Spring Boot mikroservis → talkide-storage-svc:{tag} image)
│   ├── build.gradle.kts                   (bootJar, Docker image build)
│   ├── Dockerfile
│   └── src/main/kotlin/...
└── sdk/                                   (Kotlin lib → Maven artifact app.talkide:talkide-storage-sdk)
    ├── build.gradle.kts                   (publishing: GitLab Package Registry)
    ├── src/main/kotlin/app/talkide/storage/
    │   ├── TalkideStorageClient.kt           (interface)
    │   ├── HttpTalkideStorageClient.kt       (impl: Spring RestClient)
    │   ├── TalkideStorageAutoConfiguration.kt
    │   ├── TalkideStorageProperties.kt
    │   ├── dto/PresignUploadRequest.kt
    │   ├── dto/PresignUploadResponse.kt
    │   ├── dto/PresignDownloadRequest.kt
    │   ├── dto/PresignDownloadResponse.kt
    │   ├── dto/ListFilesResponse.kt
    │   └── exception/StorageException.kt
    ├── src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    └── src/test/kotlin/...

DTO třídy (PresignUploadRequest, atd.) sdílí mezi :service a :sdk přes Gradle module dependency — :serviceimplementation(project(":sdk")) na DTO třídách. Tím je structurálně vyloučen drift mezi tím, co služba přijímá, a tím, co SDK posílá.

Tech stack:

  • Kotlin 2.x, JDK 21
  • Spring Boot 3.x BOM
  • Jackson (přes Spring Boot)
  • HTTP klient: Spring RestClient (synchronous, jednoduchý — bez Reactor závislosti)
  • Žádné AWS SDK (storage-svc je překlad layer)

Publishing:

  • MVP: privátní Maven repo přes GitLab Package Registry projektu talkide/talkide-storage-svc (https://gitlab.com/api/v4/projects/<id>/packages/maven — group-level token v scaffold gradle.properties). DO Spaces hosting odmítnut: GitLab Package Registry je nativní řešení bez dalšího infra komponentu k údržbě.
  • Verze: semver, start 1.0.0. Breaking changes = major bump (předpoklad: stabilní API). Service a SDK uvolňovány společně se stejným tagem (atomický release contractu).
  • CI: .gitlab-ci.yml v talkide/talkide-storage-svc má samostatné service:build, service:image:push, sdk:publish joby — všechny when: manual (CI cost gate, viz CLAUDE.md).
  • Verzový tag konvence: vX.Y.Z git tag triggeruje oba publish joby (service image + SDK Maven).

Telemetry / observability

SDK loguje (přes SLF4J, INFO level):

  • presignUpload(key=..., contentLength=...) — bez tokenů, bez URL.
  • presignDownload(key=...).
  • list(prefix=..., limit=...).
  • delete(key=...).

Žádné metriky v MVP (Micrometer integration je follow-up).


Backward compatibility & versioning

  • SDK 1.x: stable, žádné breaking changes v signature TalkideStorageClient.
  • Pokud storage-svc API změní (nový field v response, deprekace), SDK přidá field jako nullable + minor version bump.
  • Maře/Theovi se nikdy nesmí stát, že updated SDK rozbije starší user-app DB schema nebo controller kód.

Test Cases

GIVENWHENTHEN
user-app pod má TALKIDE_APP_STORAGE_URL + TALKIDE_APP_STORAGE_TOKEN env varsSpring context startupTalkideStorageClient bean je auto-konfigurován; injectovatelný do controlleru
user-app pod nemá TALKIDE_APP_STORAGE_URL env varSpring context startupSpring startup fail s clear error: “talkide.storage.url is required”
user-app volá storage.presignUpload("uploads/x.png", "image/png", 1024)SDK voláníHTTP POST na storage-svc /api/v1/storage/presign-upload; vrácen PresignedUpload
storage-svc vrátí 400 STORAGE_INVALID_KEYSDK voláníSDK throw StorageException.InvalidKey
storage-svc vrátí 401 AUTHENTICATION_FAILEDSDK voláníSDK throw StorageException.AuthenticationFailed (indicates env var misconfiguration)
storage-svc vrátí 429 STORAGE_QUOTA_EXCEEDEDSDK voláníSDK throw StorageException.QuotaExceeded
storage-svc vrátí 502 / connection refusedSDK voláníSDK throw StorageException.UpstreamError s cause
user-app volá storage.list("uploads/", null, 50)SDK volánívrácen PageResult<StorageObject>
user-app volá storage.delete("uploads/x.png")SDK volánížádný throw, void return
user-app volá storage.presignUpload("../escape", ...)SDK voláníSDK throw StorageException.InvalidKey (sanity check lokálně před voláním storage-svc)
Mara dostane prompt s “user wants to upload avatar”Mara generuje kódpoužije TalkideStorageClient.presignUpload(...) — NIKDY raw S3 SDK kód
Mara dostane prompt s “make file public on a URL”Mara generuje kódMara vysvětlí, že MVP signed-only, nabídne TTL=60min download URL

Out of scope (this UC)

  • Async/reactive flavor SDK (TalkideStorageClientAsync s Mono/Coroutines) — follow-up pokud user-app potřebuje.
  • TypeScript verze SDK pro FE direct calls — záměrně NE (HMAC token nesmí do browseru).
  • Local test mock (MockTalkideStorageClient) — follow-up po prvních real user-app projektech.
  • Native image / GraalVM compatibility — Spring Boot 3 to umí, ale netestováno v MVP.

Was this page helpful?

Thanks for the feedback.