Require CLAUDE.md changes to keep docs/ in sync with new features or architecture.
5.5 KiB
Architecture
Services
Four containers (production, see docker-compose.yml):
Browser → Caddy ─┬─ /admin and Strapi plugin paths → Strapi 5 (cms) → PostgreSQL (database)
└─ everything else → Nuxt 4 SSR (frontend) → Strapi REST (internal)
- database —
postgres:17-alpine. Not exposed publicly; only reachable bycmson the Docker network. Data on thepostgres-datavolume. - cms — Strapi 5, the sole source of editorial truth. No other backend framework exists in this repo; any server-side logic that isn't content management belongs in Nuxt's Nitro server, not in a new service.
- frontend — Nuxt 4 in SSR mode. Renders public pages and exposes its own REST-like endpoints
under
/api/*(Nitro), which are the only code in the repo allowed to call Strapi. - caddy — single reverse proxy, single TLS certificate, single public domain
(
PUBLIC_DOMAIN). Routes by path, not subdomain.
docker-compose.dev.yml is a local-only override (publishes ports on localhost, drops Caddy)
and must always be passed explicitly with -f docker-compose.yml -f docker-compose.dev.yml —
it's not an auto-merged override.yml, precisely so it can't be picked up by accident in
production.
Path routing (Caddy)
See caddy/Caddyfile. One site block on {$PUBLIC_DOMAIN}, one path matcher
@cms listing every Strapi/plugin top-level prefix that must bypass Nuxt:
/admin* /content-manager* /content-type-builder* /upload* /i18n*
/email* /content-releases* /review-workflows* /users-permissions* /cloud*
Everything matching @cms goes to cms:1337 (100MB body limit, for media uploads). Everything
else — including /api/* — goes to frontend:3000.
/api/* is reserved for Nuxt's own Nitro endpoints, never for Strapi. Strapi's public REST
API (/api/articles, /api/categories) is reached only from inside the Docker network, by the
Nitro server, over STRAPI_URL=http://cms:1337. The browser never sees a Strapi URL for content
— only for cover images (PUBLIC_STRAPI_URL/uploads/..., read-only, unauthenticated).
If you add a Strapi plugin that mounts its own admin API path, add its prefix to @cms in the
Caddyfile — this is the one place that list is maintained.
Request flow: reading an article
- Browser requests
/blog/my-slug→ Caddy → Nuxt SSR. frontend/app/pages/blog/[slug].vuecallsuseFetch('/api/articles/my-slug')— a same-origin call to Nuxt's own Nitro endpoint, resolved server-side during SSR (no round trip over the network in production).frontend/server/api/articles/[slug].get.tscallsstrapiFetch()(infrontend/server/utils/strapi.ts), which hitsSTRAPI_URL(internal Docker address) with a Strapi-specific query built byfrontend/server/utils/queries.ts.- The endpoint converts the article's Markdown
contentto HTML server-side (marked, viarenderMarkdown()) and derives a meta description (summarise()). The response shape isArticlefromfrontend/shared/types/blog.ts. - Nuxt renders the page with the HTML already embedded (
v-html) — no Markdown parser ships to the client, and the article body is present in the server-rendered HTML for SEO/crawlers. - The cover image
<img>tag points directly atPUBLIC_STRAPI_URL/uploads/...— the only asset the browser fetches straight from Strapi.
Request flow: publishing content
- Editor logs into
/admin(Strapi admin panel, authenticated, editor/admin only — no public sign-up, see content-model.md). - Editor writes/edits an Article (Markdown body) or Category, and publishes it (Draft & Publish).
- Strapi writes to PostgreSQL. No cache to invalidate: the next public request for that slug hits Strapi live through the Nitro endpoint.
Environment variables
Defined in .env.example (root, drives docker-compose) and
cms/.env.example (standalone Strapi dev, e.g. npm run develop outside
Docker).
| Variable | Consumed by | Purpose |
|---|---|---|
PUBLIC_DOMAIN |
caddy | Domain Caddy serves and requests a TLS cert for. |
ACME_EMAIL |
caddy | Contact email for Let's Encrypt. |
POSTGRES_DB/USER/PASSWORD |
database, cms | Postgres credentials. |
APP_KEYS |
cms | Strapi session/cookie signing keys (comma-separated). |
API_TOKEN_SALT |
cms | Salt for Strapi API token hashing. |
ADMIN_JWT_SECRET |
cms | Signs Strapi admin panel JWTs. |
TRANSFER_TOKEN_SALT |
cms | Salt for Strapi data-transfer tokens. |
JWT_SECRET |
cms | Signs users-permissions (public API) JWTs. |
ENCRYPTION_KEY |
cms | Strapi's encrypted-field key. |
STRAPI_URL |
frontend (server-only) | Internal Docker address of Strapi (http://cms:1337); mapped to NUXT_STRAPI_URL. Never sent to the browser. |
NUXT_STRAPI_TOKEN |
frontend (server-only) | Optional Bearer token for Strapi requests; unset by default. |
PUBLIC_SITE_URL |
frontend | Canonical public site URL for SEO/OG tags; mapped to NUXT_PUBLIC_SITE_URL. |
PUBLIC_STRAPI_URL |
frontend, browser | Public-facing Strapi origin for building absolute cover-image URLs; mapped to NUXT_PUBLIC_STRAPI_URL. |
All Strapi secrets have standalone copies in cms/.env.example for local development without
Docker (defaults to SQLite there, since DATABASE_CLIENT is unset).
Never commit .env files or real secret values — keep .env.example sanitized (placeholders
only).