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 chapters\n audio\n avatars\n catalogue" } 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\ncustom build + caddy-ratelimit\n\nfeatures:\n auto-HTTPS (Let's Encrypt)\n security headers\n rate limiting (per-IP)\n static error pages (502/503/504)" } 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)" } } # ─── Ops ────────────────────────────────────────────────────────────────────── ops: Ops { style.fill: "#fef9ec" watchtower: Watchtower { shape: rectangle label: "Watchtower\n(containrrr/watchtower)\n\npolls every 5 min\nautopulls + redeploys:\n backend · runner · ui" } } # ─── 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 app.ui -> storage.pocketbase: auth, progress,\ncomments, settings # ─── App internal ───────────────────────────────────────────────────────────── app.ui -> app.backend: REST API calls (server-side)\n/api/catalogue /api/book-preview\n/api/chapter-text /api/audio etc. # ─── Caddy routing ──────────────────────────────────────────────────────────── # Routes sent directly to backend (no SvelteKit counterpart): # /health /scrape* # /api/browse /api/book-preview/* /api/chapter-text/* # /api/reindex/* /api/cover/* /api/audio-proxy/* # Routes sent to MinIO: # /avatars/* # Everything else → SvelteKit UI (including /api/scrape/*, /api/chapter-text-preview/*) app.caddy -> app.ui: "/* (catch-all)\n/api/scrape/*\n/api/chapter-text-preview/*\n→ SvelteKit (auth enforced)" app.caddy -> app.backend: "/health /scrape*\n/api/browse /api/book-preview/*\n/api/chapter-text/*\n/api/reindex/* /api/cover/*\n/api/audio-proxy/*" app.caddy -> storage.minio: "/avatars/*\n/audio/*\n/chapters/*\n(presigned MinIO GETs)" # ─── 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) # ─── Ops → Docker socket ────────────────────────────────────────────────────── ops.watchtower -> app.backend: watch (label-enabled) ops.watchtower -> app.runner: watch (label-enabled) ops.watchtower -> app.ui: watch (label-enabled) # ─── Browser ────────────────────────────────────────────────────────────────── browser -> app.caddy: HTTPS :443\n(single entry point)