direction: right # ─── External ───────────────────────────────────────────────────────────────── novelfire: novelfire.net { shape: cloud style.fill: "#f0f4ff" } kokoro: Kokoro-FastAPI TTS { shape: cloud style.fill: "#f0f4ff" } letsencrypt: Let's Encrypt { shape: cloud style.fill: "#f0f4ff" } browser: Browser / iOS App { shape: person style.fill: "#fff9e6" } # ─── Init containers (one-shot) ─────────────────────────────────────────────── init: Init containers { style.fill: "#f5f5f5" style.stroke-dash: 4 minio-init: minio-init { shape: rectangle label: "minio-init\n(mc: create buckets)" } pb-init: pb-init { shape: rectangle label: "pb-init\n(bootstrap collections)" } } # ─── Storage ────────────────────────────────────────────────────────────────── storage: Storage { style.fill: "#eaf7ea" minio: MinIO { shape: cylinder label: "MinIO :9000\n\nbuckets:\n libnovel-chapters\n libnovel-audio\n libnovel-avatars\n libnovel-browse" } pocketbase: PocketBase { shape: cylinder label: "PocketBase :8090\n\ncollections:\n books chapters_idx\n audio_cache progress\n scrape_jobs app_users\n ranking" } valkey: Valkey { shape: cylinder label: "Valkey :6379\n\n(presign URL cache\nTTL-based, shared)" } meilisearch: Meilisearch { shape: cylinder label: "Meilisearch :7700\n\nindices:\n books" } } # ─── Application ────────────────────────────────────────────────────────────── app: Application { style.fill: "#eef3ff" caddy: caddy { shape: rectangle label: "Caddy :443 / :80\n(reverse proxy\nauto-HTTPS via Let's Encrypt)" } backend: backend { shape: rectangle label: "Backend API :8080\n(Go — HTTP API server)" } runner: runner { shape: rectangle label: "Runner :9091\n(Go — background worker\nscraping + TTS jobs\n/metrics endpoint)" } ui: ui { shape: rectangle label: "SvelteKit UI :3000\n(adapter-node)" } } # ─── Init → Storage deps ────────────────────────────────────────────────────── init.minio-init -> storage.minio: create buckets {style.stroke-dash: 4} init.pb-init -> storage.pocketbase: bootstrap schema {style.stroke-dash: 4} # ─── App → Storage ──────────────────────────────────────────────────────────── app.backend -> storage.minio: blobs (chapters, audio,\navatars, browse) app.backend -> storage.pocketbase: structured records\n(books, progress, jobs…) app.backend -> storage.valkey: cache presigned URLs\n(SET/GET with TTL) app.runner -> storage.minio: write chapter markdown\n& audio MP3s app.runner -> storage.pocketbase: read/update scrape jobs\nwrite book records app.runner -> storage.meilisearch: index books on\nscrape completion app.ui -> storage.valkey: read presigned URL cache\n(replaces in-process Map) # ─── App internal ───────────────────────────────────────────────────────────── app.ui -> app.backend: REST API calls\n(server-side) # ─── Caddy routing ──────────────────────────────────────────────────────────── app.caddy -> app.ui: /* (proxy) app.caddy -> app.backend: /api/*, /health (proxy) app.caddy -> storage.minio: /s3/* (proxy, internal only) # ─── External → App ─────────────────────────────────────────────────────────── app.runner -> novelfire: scrape\n(HTTP GET) app.runner -> kokoro: TTS generation\n(HTTP POST) app.caddy -> letsencrypt: ACME certificate\n(TLS-ALPN-01) # ─── Browser ────────────────────────────────────────────────────────────────── browser -> app.caddy: HTTPS :443\n(single entry point)