Doctor checks the client-address configuration of the process it runs
in, and operators run it as `migrate`. The proxy overlay set
trusted-proxy mode on `api` only, so Doctor reported "Direct
client-address mode" for an installation that trusts its proxy.
- compose.proxy.yaml defines the three variables once, as a YAML
anchor, and applies it to both `api` and `migrate`. Rendered with
`docker compose config`, `api` is unchanged and `migrate` now
matches it, keeping NODE_EXTRA_CA_CERTS from the base.
- The example deploy/.env sets
COMPOSE_FILE=compose.yaml:compose.proxy.yaml, as the instance already
does. Plain `docker compose run --rm migrate ...` in the README then
runs with the overlay; it used to read compose.yaml alone.
- Every client-address check in Doctor now says it checked the
environment Doctor runs with, and how to read the running API's own
value (`docker compose exec api printenv
EKKLESIA_CLIENT_ADDRESS_MODE`, confirmed to work in the image).
- The deploy README explains why operator commands need the same files
as the stack.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An installation can now serve the Dashboard and the API on sibling
hostnames (dash./api.) beside the default same-origin topology, from one
image and with configuration alone.
- The Dashboard learns its API origin at run time: the built page carries
one empty <meta name="ekklesia-api-origin">, which the API fills with its
identity base URL's origin at startup, before computing the page's ETag.
The shell accepts only a canonical http(s) origin and otherwise fails
closed without making a request. The build-time EKKLESIA_API_BASE_URL is
gone from the bundle; the development server injects the same element.
- The display host stays on the API's origin, so its device cookies,
requests, and offline cache are unchanged.
- The API trusts its own origin (trusted origins plus the identity base
URL), as Better Auth already does, so the display page's mutations pass
the origin check without operators listing it.
- Doctor validates EKKLESIA_DASHBOARD_ORIGIN exactly as the API's startup
does (it previously skipped it), reports the topology without printing
hostnames, and warns when split hostnames are not siblings.
- The module UI contract states the CORS header limit (safelisted headers
plus Content-Type, safelisted response headers only).
- Reference deployment: Caddyfile.split.example (Dashboard hostname serves
/ and its script; API hostname serves /api/*, /display/*, and the
display script; each refuses the other's paths) and
EKKLESIA_DASHBOARD_HOSTNAME in the proxy overlay and env examples.
- The deploy smoke test runs either topology (CI runs both). The split run
installs the module template as a test-only module, checks per-hostname
routing and exact CORS, then drives a digest-pinned headless Chromium
over the DevTools protocol, with no added dependency: sign-in on the
Dashboard hostname, the session cookie host-only on the API hostname,
the module UI imported cross-origin and its credentialed request
succeeding, the display and its service worker on the API hostname, and
sign-out.
ADR 0045 is marked Accepted.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bump the root and every platform workspace package from 0.1.0 to 0.1.1, so
the platform reports the version the v0.1.1 tag will carry. Module discovery
checks compatibility.platform against the root version and
compatibility.moduleSdk against @ekklesia/module-sdk; both stay inside every
existing ^0.1.0 range, so no module needs a new range. Everything since v0.1.0
is additive (health endpoints, Dashboard serving, production dependency
declarations, the deployment image and Compose project).
templates/module keeps 0.1.0: it is a module, versioned on its own, and its
manifest, source, and tests carry its own product version.
Mark ADR 0044 Accepted. Slices 1-4 are implemented and proven by CI's image
build and deploy smoke; slice 6 (scheduled backups and the restore
procedure) remains its open item.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Implement ADR 0044's reference deployment as a base Compose file plus
overlays, so an installation that already runs a proxy can use the base
alone.
- compose.yaml: api, postgres, object-storage, and the operator-only
migrate (profile ops, plan by default). No published port. api and
migrate run init, read-only, tmpfs /tmp, cap_drop ALL,
no-new-privileges. postgres is only on the internal data network.
- TLS everywhere from one installation-private CA: PostgreSQL TLS 1.3
only, with pg_hba rejecting every non-TLS connection, and the API on
verify-full via EKKLESIA_DATABASE_CA (PEM text). SeaweedFS serves S3
over HTTPS (ADR 0030 requires https in production), trusted by the
API through NODE_EXTRA_CA_CERTS. No Core change.
- compose.proxy.yaml + Caddyfile.example: Caddy pinned by digest, ACME
HTTP-01 (DNS-01 documented), routes /, /api/*, /display/*, /assets/*
to api and refuses everything else; the storage hostname preserves
Host. The API trusts exactly the proxy's fixed /32 on an edge network
whose dynamic range excludes the fixed addresses; Caddy overwrites
X-Forwarded-For and strips caller-supplied forwarding headers.
- compose.tunnel.yaml: cloudflared pinned by digest, token from its own
env file; Caddy honors CF-Connecting-IP only from its fixed address.
- Four per-service root-owned env files, examples with no values;
scripts/init-tls.sh (idempotent, never overwrites a key) and
scripts/gen-secrets.sh (prints only).
- CI: pnpm deploy:config in ci:verify for all three combinations, and a
deploy-smoke job (scripts/smoke-deploy.sh) proving readiness, the
Dashboard, signed storage delivery, the client-address chain, initial
owner sign-in/out, verify-full database TLS, and Doctor through the
proxy, against disposable services. tests/deploy.test.ts pins the
structural guarantees.
- ADR 0044 (still Proposed) amended to match: API-to-storage HTTPS,
DNS-01 needs a custom Caddy build and tunnel-only needs DNS-01, the
tunnel's origin TLS settings live in the Cloudflare dashboard, port
80 serves HTTP-01, and secrets are one env file per service.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A module that imports a package at run time without declaring it a
production dependency now fails the image build instead of the API's
startup, since the runtime receives production dependencies only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add deploy/Dockerfile and .dockerignore (ADR 0044): one image for the
api service and every operator-run CLI, built from an already-assembled
workspace (the platform plus whatever modules/<id> the context holds).
- node:24.18.0-trixie-slim pinned by index digest; pnpm via Corepack at
the root packageManager version.
- Build stage: frozen install, modules:validate, migrations:validate,
build. Runtime: production-only node_modules plus every dist/, core
migrations, and each module directory, in the repository layout the
API and CLIs resolve paths from; root-owned, run as node.
- NODE_ENV=production and EKKLESIA_API_HOST=0.0.0.0 inside the image
only; the platform default stays 127.0.0.1. Configuration comes only
from the environment; no .env enters the context or any layer.
- HEALTHCHECK on /api/v1/health/ready with Node's fetch; OCI labels for
source, revision, and version.
- CI builds the image without modules: build only, no credentials, no
push.
- deploy/README.md documents the image, the read-only production
posture (verified), init: true, and the exact CLI commands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>