The entry point

A single Caddy proxy on port 8080 is the only surface exposed to the browser. It routes by path: / goes to the Nuxt admin, /api/* is rewritten to the Hono API, /storage/* is rewritten to the storage service. S3/Minio is never reachable from outside — only the storage service talks to it.

Proxy (Caddy)

:8080

Routes by path: / → admin, /api/* → api (strip prefix), /storage/* → storage (strip prefix). Handles TLS in production.

Admin (Nuxt 4)

:3000internal only

Nuxt 4 admin UI: story editor, component schema editor, datasources, assets, access tokens, audit log.

API (Hono)

:3001internal only

Hono + Bun. Hosts CDN v2 (Storyblok-compat), Management v1, MCP (/v1/mcp), auth, webhooks. Only service that touches the Postgres schema.

Storage (Hono)

:3002internal only

Hono + Bun. Upload, download, delete and on-the-fly /m/ image transforms. Only service with S3 credentials. Uses HMAC tokens minted by the API for upload auth.

Postgres

:5432internal only

Canonical store. Stories, drafts, versions, components, datasources, assets, tokens, audit log. Schema owned by Drizzle.

Minio (S3)

:9000internal only

S3-compatible object store for asset blobs. Bucket layout: spaces/<id>/assets/<n>/<filename>. Transforms live under /m/. Never exposed externally.

Redis (optional)

:6379internal only

Optional CDN cache. If REDIS_URL is empty the cache client is a silent no-op — no try/catch, no errors, just slower responses.