Repository navigation
Choose the platform stack #1
Description
Activity
- added a parent issue
on Oct 7, 2026 - addedtype:spikeResearch that answers a question and ends thereResearch that answers a question and ends theretrack:fullNeeds evidence: brief, research, plan, execute, reviewNeeds evidence: brief, research, plan, execute, reviewstage:briefBeing briefedBeing briefed
on Oct 7, 2026 Time: issue-1
Generated by
worklog renderfromtime.jsonl. Do not edit by hand.Step Estimate Started Finished Wall Active Sessions brief 30m 2026-10-07 11:01 2026-10-07 11:03 1m 1m 3da8d26d (claude-code) research 4h 2026-10-07 11:03 2026-10-07 23:03 12h 59m 3da8d26d (claude-code) Total 4h 30m 12h 2m 1h Active time counts agent turns plus up to 5 minutes of each wait for a person, and no single gap
counts more than 30 minutes.—means nothing was recorded.workflow · claude-code · 2026-10-07 23:04
- addedstage:researchBeing researchedBeing researchedand removedstage:briefBeing briefedBeing briefed
on Oct 7, 2026 Research: Choose the platform stack
Status: Answered (two choices are left for the user: where the fake payment gateway lives, and opt-in container log collection) · Evidence base:
5db9dab(clean), plusd0573a0for the parent docs and the sibling spikes at their SHAs · 2026-10-07Summary
How it runs. The platform runs everything with Docker Compose alone; Tilt was rejected. Each app runs from a GHCR image pinned by digest, and an opt-in override builds it from your local checkout when you're working on it. Each app repo declares its own needs in a small
compose.platform.yamlfragment, listing its required environment variables by name. The platform pulls that fragment in at a pinned commit and supplies the values, the routing and the startup order.Commands. The commands are a thin Make wrapper over POSIX shell scripts that also run without Make.
Bootstrap. Bootstrap generates per-install dev secrets and a local certificate authority. The CA is name-constrained to
rsl-commerce.test, which plain mkcert can't do, and it signs one certificate covering all eight hosts. Bootstrap trusts the CA with each OS's own tools; on WSL2 that's a Windows user-store import that needs no admin. It prints the hosts-file lines, and applies them only when asked, with a single elevation.Proxy. nginx-unprivileged is the TLS proxy. It is already the org's static-app image, and it is the only candidate that ran with no capability exceptions.
Services. PostgreSQL 18, Valkey 9.1 in place of Redis, RabbitMQ 4.3, Mailpit and Meilisearch (MIT community build) all ran healthy as non-root on read-only filesystems. Valkey is BSD-licensed and matches what managed clouds now offer. The backend's go-redis client passed every operation it needs against Valkey.
Observability.
grafana/otel-lgtm: one container, about 600 MiB.Tests. Playwright end-to-end tests run against the composed stack with a fake Authorize.Net, so no secret is ever needed; a real-sandbox run is an optional manual job. Schemathesis runs contract tests. A committed
compose.images.yamlholds the known-to-work-together matrix, which Dependabot keeps current.Docs. The engineering docs site is VitePress 2, matching the design system, with a Swagger UI API reference (the only renderer clean under a fully strict CSP) and Mermaid diagrams pre-rendered to SVG.
Confidence is high for services, the proxy, observability and tests. It is medium-high for bootstrap, because browser trust of a name-constrained root was not tested: no trust store was touched.
Answers
RQ Answer Confidence RQ-1 Docker Compose v5.x (5.6.0 latest; the old "v2" line is now v5), with profiles, include,depends_on: service_healthy,develop.watchandup --wait, all exercised. Tilt is rejected.
Apps join from GHCR images pinned by digest by default, with an opt-in local build per app. Public GHCR images pulled anonymously.
Contract: each app repo shipscompose.platform.yamlfor its own services only. Env names are${VAR:?}, so a missing value fails and names the variable. Each fragment includes its healthcheck and DE-5 hardening, with no ports and no infra hostnames. The platform includes it through a Git remote at a pinned SHA, plus its own wiring.High (Compose) / medium-high (contract) RQ-2 Commands: Make as a thin entry over POSIX shscripts, compatible with GNU Make 3.81 (macOS). just is the runner-up; Task and mise are heavier and need template escaping.
Bootstrap: check prerequisites, generate.envand the CA and certificate, trust the CA, then print the hosts lines, with an opt-inmake hoststhat elevates once.
Secrets: a POSIX script writes a gitignored, mode-600.envand fills only missing keys.
Authorize.Net credentials are optional and empty by default; without them payments are disabled. Seed isrun --rm api seed(the backend's own command); reset isdown -vafter confirmation.Medium-high RQ-3 nginx-unprivileged, stable 1.30 alpine-slim pinned by digest, with a file config. All three proxies passed 8 hosts, HTTP/2, HSTS added, the app's CSP unchanged, WebSocket 101, and %2Fpreserved. Only nginx did it as non-root with no capability exceptions. Caddy needsNET_BIND_SERVICE. Traefik's label mode needs the Docker socket (root-equivalent), and it had 58 advisories in 2026.
TLS: a local root name-constrained torsl-commerce.testplus one leaf with 8 explicit SANs. A foreign name signed by the same root was rejected. Local HSTS ismax-age=86400; includeSubDomains, with no preload.Medium-high RQ-4 Measured healthy, non-root, read-only and with all capabilities dropped:
• PostgreSQL 18.6 (46 MiB)
• Valkey 9.1.2 (7 MiB)
• RabbitMQ 4.3.6 withdefault_queue_type = quorum(171 MiB)
• Mailpit 1.31 (11 MiB)
• Meilisearch 1.54 community/MIT (38 MiB, analytics off)
Mailpit and Meilisearch need a two-line derived image so their data directory isn't root-owned.
Valkey over Redis 8: BSD-3 vs RSALv2/SSPLv1/AGPLv3; ElastiCache offers Valkey up to 9.1 but Redis OSS only up to 7.1. go-redis v9.23.0 passed the backend's operations on Valkey 9.1.2.High RQ-5 grafana/otel-lgtm0.35.0, which its README says is for development: one OTLP endpoint, Grafana atobserve., and dashboards provisioned as files. Traces, logs and metrics all arrived. It uses about 600 MiB idle and is a 873 MB compressed image. It runs as non-root and read-only with extra tmpfs mounts.
The separate stack is about 485 MiB across 5 containers, with more config. SigNoz needs at least 4 GB; Jaeger has no logs.
Logs: apps send OTLP (OTel Go logs went stable in v1.47.0); infra stdout goes throughmake logs.Medium-high RQ-6 E2E: Playwright on the composed stack, using a fake Authorize.Net (the backend's gateway base URL points at it). Playwright intercepts the browser's post to test.authorize.net, so checkout's CSP stays unchanged and no secret is needed. A real-sandbox job is optional, manual or weekly, and never required. Contract: Schemathesis 4.29.4, pinned, with fixed seeds on PRs and more examples nightly; Dredd is archived.
Matrix:compose.images.yamlwithname:tag@sha256; Dependabotdocker-composebumps tag and digest together. Runner: standard public, 4 vCPU / 16 GB / 14 GB SSD; per-PR E2E leaves out the observability profile.High RQ-7 VitePress 2.0 alpha, pinned exactly; its CSP (2 script hashes plus inline styles) gave 0 violations.
API reference: Swagger UI 5.33.1, the only renderer with 0 violations under the fully strict policy (433 kB gzip, 1 dependency). Scalar is the runner-up but needs hardening and is 2.3× larger; Redoc and Elements break under strict styles.
Diagrams: Mermaid pre-rendered to SVG (0 JS, 0 violations). Content: link out to READMEs; an ADR index fetched at the matrix's SHAs fromdocs/adr/; the spec from a backend release asset.
Local search; nginx-unprivileged; Pages off until asked.High Recommendation
1. Layout (RQ-1, RQ-2, RQ-6; FA7-FA29, FC16-FC17)
platform/ compose.yaml # name; infra services; include: [<app fragment>, compose/compose.<app>.yaml] per app compose/compose.<app>.yaml # platform wiring per app: literal image name:tag@sha256 (the matrix), depends_on, networks compose/local/<app>.yaml # opt-in: build from ${<APP>_SRC:-../<app>}, pull_policy: build proxy/ # nginx conf per host, CA/leaf generation scripts observability/ # Grafana dashboards + provider YAML docs/ # VitePress 2 engineering docs + Dockerfile (nginx-unprivileged) tests/ # Playwright E2E, smoke, Schemathesis config scripts/ # bootstrap.sh gen-env.sh ca.sh trust.sh hosts.sh doctor.sh (POSIX sh) .env.example .env (gitignored, 600) certs/ (gitignored) Makefile- Profiles: no profile for the proxy, infra and apps;
observability;test. upandreset:upalways uses--wait, andresetuses--profile '*'.- Minimum version: Compose 5.0.
Reconciled matrix shape (inference, verify at scaffold).
- The conflict: RQ-1 proposed image digests through env vars in
versions.env. RQ-6 found that Dependabot skips images set by env vars and scans only files namedcompose*.yamlin the configured directories (FC16-FC17). - The resolution: app fragments don't set
image:. The platform's wiring filescompose/compose.<app>.yamlset it as a literalname:tag@sha256. They merge with the fragment throughinclude: path: [fragment, wiring], which is the documented merge (FA8). Dependabot then watchesdirectories: ["/", "/compose"]. - Fragment refs: Dependabot cannot bump the Git refs of the fragments. Tie each fragment ref to the app's release tag, and bump image and fragment together with a small scheduled workflow that opens a PR. Reading public repos needs no token, and
GITHUB_TOKENcan open PRs in its own repo. This is homegrown; decide it in the ADR.
2. Commands and bootstrap (RQ-2; FA30-FA61)
Command What it does bootstrapIdempotent setup up [PROFILES=…] [LOCAL=backend,…]Start; LOCALbuilds the named apps from local checkoutsdownStop; keeps data resetDelete volumes after confirmation; keeps .envand the certificatesseeddocker compose run --rm backend-api seedlogs [s=…]Follow logs statusHealth, certificate, hosts lines, and whether payments are enabled doctorRead-only checks: XDG_RUNTIME_DIR, Docker Desktop disk health, ports 80 and 443hosts,trustThe two steps that need elevation CA, merging RQ-2 and RQ-3:
ca.shusesopensslto create a root withnameConstraints=critical,permitted;DNS:rsl-commerce.test,pathlen:0and a validity of 825 days or less. It then issues one leaf with the 8 SANs. The root key stays gitignored per machine.- mkcert is not needed. It is unmaintained (last release 2022-04-26), its root is unconstrained, and it misses Chrome M146's new Linux NSS path (mkcert #671).
- Trust uses each OS's own tools:
- WSL2:
powershell.exe Import-Certificate -CertStoreLocation Cert:\CurrentUser\Root. No admin; one Windows warning dialog. This covers Edge, Chrome and Windows Firefox. - macOS:
sudo security add-trusted-cert -d -k /Library/Keychains/System.keychain, plus NSS for Firefox. - Linux:
sudocopy plusupdate-ca-certificates, andcertutilinto both~/.pki/nssdband~/.local/share/pki/nssdb.
- WSL2:
Hosts: printed by default.
make hostswrites a marked# BEGIN rsl-commerce/# ENDblock, idempotently and with one elevation. On WSL2 that's a single UAC prompt throughStart-Process -Verb RunAs, the DDEV precedent. Bootstrap reconciles existing entries and never deletes lines it didn't write.Manual steps left:
- Everyone: install Docker, clone, run
make bootstrap, and optionally paste Authorize.Net sandbox credentials. - WSL2: accept one certificate dialog and one UAC prompt;
sudo apt install makeif it's missing. - macOS: the sudo password twice;
brew install nssfor Firefox. - Linux:
sudo apt install make libnss3-tools, and the sudo password twice.
Secrets:
gen-env.shis POSIXshusing/dev/urandom, runs underumask 077, fills only empty keys and prints names only.- The Postgres password is delivered as a Compose secret into
POSTGRES_PASSWORD_FILE. - Add key-name patterns and an Authorize.Net pattern to git-secrets: random hex isn't caught by the existing patterns (FA55).
3. Proxy and headers (RQ-3; FB1-FB36)
- nginx-unprivileged stable alpine-slim, pinned by digest, with one
serverblock per host and a default server usingssl_reject_handshake on. - A
map $http_upgradeWebSocket route onapi., with a 1-hour read timeout. - Upstream names come from the image's
/etc/nginx/templatesenvsubst. - The proxy sets TLS, HSTS (
max-age=86400; includeSubDomains) andX-Forwarded-*. Apps set CSP, COOP, CORP,nosniffand caching. The proxy never sets a header the app sets. - Rejected:
- Caddy (runner-up) needs a DE-5 exception.
- Traefik would need a file provider, which gives up its label discovery, plus the advisory load.
- Caddy
tls internaland step-ca were rejected for the CA.
4. Services (RQ-4; FB37-FB53)
Service Image (pin digest) Notes PostgreSQL postgres:18(18.6)user: 999:999; volume/var/lib/postgresql(a PG18 change); tmpfs/tmpand/var/run/postgresql; health checkpg_isreadyValkey valkey/valkey:9.1(9.1.2)BSD-3; user: 999:999;/data; health checkvalkey-cli ping; the backend uses a neutralCACHE_URL=redis://valkey:6379/0and avoids Redis-8-only commandsRabbitMQ rabbitmq:4.3-management(4.3.6)user: 999:999;default_queue_type = quorum; the management UI only on127.0.0.1:15672, not a 9th hostMailpit axllent/mailpit:v1.31plus a 2-line derived imageSMTP 1025 internal; UI at mail.Meilisearch getmeili/meilisearch:v1.54(community, MIT) plus a 2-line derived imageMEILI_NO_ANALYTICS=true; generated master key- Platform-provided values the backend's fragment declares:
DATABASE_URL,CACHE_URL,AMQP_URL,SMTP_ADDR,MEILI_URLand its key, andOTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318withhttp/protobufandOTEL_SERVICE_NAME. - Backend rules: quorum queues with a DLX and
x-delivery-limitare declared explicitly; at-least-once delivery needsoverflow=reject-publish; trustX-Forwarded-Protoonly from the proxy; apps never send HSTS.
5. Observability (RQ-5; FB54-FB71)
grafana/otel-lgtm:0.35.0, pinned, in theobservabilityprofile.- It runs as uid 65534 with a read-only root and tmpfs on
/tmp,/dataand/var/tempo. Grafana is atobserve., and OTLP 4317/4318 stays internal. - Dashboards and datasources are provisioned from files, and the Grafana admin password comes from
.env. - Logs:
- the Go backend uses the OTLP log exporter, stable since v1.47.0;
- the storefront logs to stdout, because the Node logs SDK is still "Development";
- infra stdout goes through
make logs.
- Rejected: filelog on Docker's log files (impossible on Docker Desktop), the Loki driver (no Windows support), Promtail (end-of-life since 2026-03-02).
- The fallback is the separate collector, Prometheus, Loki, Tempo and Grafana stack.
6. Tests, the matrix and CI (RQ-6; FC1-FC25)
Tier 1 runs on every PR and nightly, with no secrets.
- Setup: the
testprofile with the backend set toPAYMENTS_GATEWAY=fake. The fake Authorize.Net handlesgetHostedPaymentPageRequest, transaction details, and HMAC-signed webhooks using a generated dev key. - Browser leg: Playwright
page.route('https://test.authorize.net/payment/payment')serves the fake hosted page. - Assertion: the order appears in both storefront and admin.
- No hosts file in CI: Chromium gets
--host-resolver-rules=MAP *.rsl-commerce.test 127.0.0.1, and curl gets--resolve.
Tier 2 is manual or weekly.
- It uses the real sandbox with optional secrets: test card 4111…, and ZIP 46282 for declines.
- It skips itself when the secrets are absent and is never a required check.
- Outcomes are confirmed through the transaction-details API and reconciliation, because webhooks can't reach a runner.
Smoke tests:
up --waitplus acurl --resolvesweep over every host.Contract tests:
schemathesis/schemathesis:4.29.4@sha256:16d21ecb…runs on the compose network against the pinned backend spec. PRs use--max-examples 50 --seed <fixed>; nightly runs use 200 or more with a random seed.The matrix:
- The
compose/compose.<app>.yamlimage lines are the matrix (see §1). - Dependabot
docker-composeruns daily in two groups: our GHCR images with no cooldown, since GHCR has no publication dates, and third-party images with a 7-day cooldown. - Each bump PR runs Tier 1, so main is the known-good set.
- Nightly runs the full E2E, Schemathesis and Grype on the pinned set, plus a non-blocking canary against each app's latest image.
CI workflows:
ci.yml,nightly.yml,sandbox-e2e.yml(manual) andcodeql.yml, all on the standard runner. Measure peak RAM and disk in the first run, because the 14 GB SSD is the tight resource.DE-4 for this repo:
- CodeQL
actionsplusjavascript-typescript; - re-pin actions by SHA;
- Dependabot for github-actions, npm, docker and docker-compose;
docker compose config --quietper profile;- shellcheck;
- hadolint on the docs Dockerfile and the derived images;
- Grype on every third-party image in the matrix and on the docs image;
- dependency-review;
- push protection.
7. Engineering docs site (RQ-7; FC26-FC46)
- VitePress 2.0.0-alpha.20, pinned exactly. Its theme comes from the design-system's tokens package once that is published.
- CSP:
script-src 'self'plus the 2 VitePress inline-script hashes, generated per build and checked in CI, andstyle-src 'self' 'unsafe-inline'. - API reference: Swagger UI 5.33.1 on its own static page at
/api/. Its init script is external, and "try it out" is off or limited toapi.locally. Its nginxlocationcan drop'unsafe-inline'. - Diagrams: Mermaid is pre-rendered to SVG in the pinned Playwright image and embedded with
<img>. C4-style views are Mermaid flowcharts; Mermaid's own C4 syntax is experimental. - Content:
- READMEs are linked out to GitHub.
- The ADR index is fetched at build time from each repo's
docs/adr/NNNN-slug.mdat the matrix's SHAs, throughGITHUB_TOKEN, with a gitignored cache for offline builds. - The OpenAPI spec comes from the backend's
openapi.jsonrelease asset. - Submodules are rejected, because they would couple platform to the parent's layout.
- Search, serving, links: local search; nginx-unprivileged, read-only, with all capabilities dropped. The site links to
design.rsl-commerce.test. - GitHub Pages is off until asked. Pages can only deliver a CSP as a
<meta>tag, which cannot carryframe-ancestors.
Risks and unknowns
- Untested trust and name constraints. Browsers enforcing name constraints on a user-added root, and Windows trust from WSL, are untested, because no trust store was touched. Verify both in the first bootstrap run. Falling back to mkcert costs nothing in the flow.
- Docker Desktop instability on this machine.
- During the research its engine hung with disk I/O errors, the CLI crashed with SIGBUS, and the machine crashed at 11:38. After the crash, two pulled images had truncated layers and segfaulted until re-pulled (FB71).
doctorshould check for this, and digest pins plus health gates make corrupt pulls fail visibly.
- Silent Compose merges. A service defined twice is merged silently in v5.6.0, despite the docs (FA22). Use prefixed service names plus a CI check that fragments only declare their own prefix.
- Fragment-ref bumps need the small homegrown workflow. GHCR packages start private, so each app's CI must publish them as public.
- Browser-leg intercept untested. Intercepting the browser's cross-origin form post with Playwright is untested. The fallback is a test-profile-only
form-actionfor the fake's origin. - Runner capacity is unmeasured: otel-lgtm (873 MB) and the Playwright image (956 MB) make up most of about 2.4 GB of compressed images.
- Schemathesis is stochastic. It missed a planted 500 at 50 examples and found it at 200.
- Valkey compatibility rests on our probe. go-redis makes no official Valkey statement; valkey-go is the fallback.
- Host and service names drift across docs. The root README lists 5 hosts and the platform README 7, while DE-11 says 8. README text still says Redis. Those edits are deferred follow-ups.
- Existing state on this machine. The Windows hosts file has 7
rsl-commercelines, withdesign.missing.CurrentUser\Rootholds an old mkcert leaf. Bootstrap must reconcile. - Experiment side effect. Docker created an empty, root-owned
/var/lib/docker/containersdirectory in the WSL distro during a bind-mount probe.sudo rmdir /var/lib/docker/containers /var/lib/dockerremoves it.
What this means for the plan
Candidate decisions for the platform ADR:
- P-D1: Compose ≥ 5.0 only; the layout, profiles and
--wait; no Tilt. - P-D2: per-app
compose.platform.yamlcontracts (rules in §1); platform wiring files carrying the literal image pins; Git remote includes at release refs; local mode via sibling paths. - P-D3: Make over POSIX scripts, with the command list above.
- P-D4: a name-constrained openssl CA plus an 8-SAN leaf; trust through OS tools; hosts printed, applied on request.
- P-D5:
gen-envdev secrets, Compose secrets and git-secrets key patterns; optional Authorize.Net credentials, with payments disabled without them. - P-D6: nginx-unprivileged proxy; local HSTS of one day; headers split between proxy and apps.
- P-D7: the service table, Valkey instead of Redis, and DE-5 hardening with two derived images.
- P-D8: otel-lgtm, OTLP logs plus
make logs. - P-D9: Tier 1/Tier 2 E2E with a fake gateway, smoke tests, and Schemathesis.
- P-D10: the matrix and Dependabot update flow; CI workflows; DE-4 for this repo.
- P-D11: VitePress 2 docs, Swagger UI, pre-rendered Mermaid, ADR index fetched at the matrix SHAs, Pages off.
What each other repo must provide:
Repo Must provide Every app A public GHCR image per release with semver tags and the org.opencontainers.image.sourcelabel;compose.platform.yamlper the rules; a health endpoint; ADRs atdocs/adr/NNNN-slug.mdbackend Idempotent seedandmigratesubcommands; ahealthchecksubcommand for distroless; payments disabled without credentials; a configurable Authorize.Net base URL and Signature Key; the fake gateway, if that's where it lives;openapi.jsonas a release asset; test identities; OTLP logscheckout The hosted-form URL comes from the backend's session response, not hard-coded design-system A docs image served at design.; the theme and tokens package for the platform's docscommerce READMEs updated for 8 hosts and Valkey Verified by the lead before posting:
- Releases:
- docker/compose v5.6.0 (2026-10-02); v5.0.0 delegates builds to Bake;
- mkcert latest v1.4.4 (2022-04-26); issue #671 (Chromium M146 NSS path) is open;
- just 1.58.0;
- opentelemetry-go v1.47.0 (2026-10-02): "the first stable release of the OpenTelemetry Go Logs API and SDK";
- grafana/docker-otel-lgtm v0.35.0 (2026-10-02);
- valkey 9.1.2 (BSD-3-Clause).
- Licences and support:
- Redis licences page: 8.0+ is RSALv2/SSPLv1/AGPLv3; 7.4 is RSALv2/SSPLv1; 7.2 and earlier are BSD-3;
- ElastiCache engine versions: Valkey 7.2 through 9.1, and "all Redis OSS versions 7.1 and before".
- Runner limits: GitHub-hosted runners for public repos are 4 CPU, 16 GB RAM and 14 GB SSD, free.
- Docs and testing tools:
- npm: swagger-ui-dist 5.33.1 (Apache-2.0), @scalar/api-reference 1.73.0, redoc 2.5.4, @stoplight/elements 9.0.27, mermaid 12.1.0;
- vitepress-plugin-mermaid peers
vitepress ^1andmermaid 10 || 11; - apiaryio/dredd is archived; Schemathesis 4.29.4 on PyPI.
- Cleanup and side effects:
- The hosts-file checksums (
57c32caf…,0c1f8948…) and certificate counts (Linux 256 and 8; Windows CurrentUser 124, LocalMachine 115) are unchanged from the pre-experiment baseline. - No containers remain.
- The
/var/lib/docker/containersside effect is confirmed.
- The hosts-file checksums (
Evidence is in the
research:evidencecomment. Its finding prefixes:- FA: orchestration and bootstrap (RQ-1, RQ-2);
- FB: proxy, services and observability (RQ-3 to RQ-5);
- FC: tests and the docs site (RQ-6, RQ-7).
Related correction: the OTel Go logs status in commerce-backend#1's research was revised in place on 2026-10-07.
workflow · claude-code · 2026-10-07 12:13
- Profiles: no profile for the proxy, infra and apps;
Research evidence: Choose the platform stack
Evidence base: platform
5db9dab, rootd0573a0, backend38a8548, agent-workflow-toolingc1d006c; the decisions in commerce#3 and the sibling spikes (read-only). All sources accessed 2026-10-07.The experiments ran on WSL2 Ubuntu 22.04 with Docker Desktop (Engine 29.8.0, then 29.8.2 after the 11:38 crash). Everything cited is from runs captured before the crash or redone after it; nothing is recalled from memory.
Context
-
FA1 [verified-code] The boundary rules:
- "Applications own their contract with infrastructure … The platform owns provisioning";
- no application code;
- local is not production;
- minimal manual steps;
- dev secrets only;
- every service meets a need.
(
platform/AGENTS.md:8-19 @ 5db9dab) -
FA2 [verified-code] Responsibilities (
platform/README.md:10-18), "Clone it, bootstrap it, bring it up" (:3-4), one hosts line per address (:32), and "Does not own" (:36-38), all @5db9dab. -
FA3 [verified-code] Contracts include "the configuration each application declares and the platform provides", and "no application assumes how the platform provisions" (
AGENTS.md:11-17). The README rules out "Kubernetes for the résumé" (README.md:59-61) and lists only 5 addresses (README.md:20-26), @d0573a0. -
FA4 [verified-code] The backend owns "migrations, constraints and seed data" (
backend/README.md:21) and "declares what it needs" (:29-30), @38a8548. -
FA5 [verified-code] Submodules are siblings with relative URLs. (
.gitmodules:1-21 @ d0573a0) -
FA6 [sourced] Decisions already taken:
- DE-5 containers and DE-11 eight hosts (commerce#3);
- backend D5: payment port,
unknownstate, reconciliation, webhook HMAC (commerce-backend#1); - HSTS at the proxy, CSP from the apps (commerce-admin#1 D5, commerce-checkout#1 D4).
RQ-1: Orchestration and how apps join (FA)
-
FA7 [sourced] Compose v5.6.0 (2026-10-02) is the latest; Docker Desktop here bundles v5.5.1. v5.0.0: "Internal builder has been removed, build is delegated to Docker Bake". v5.6.0 adds partial support for jobs. (docker/compose releases; re-checked by the lead)
-
FA8 [sourced]
include:- needs Compose 2.20.3+;
- each included file is loaded "as an individual Compose application model";
- relative paths resolve against the included file;
pathlists merge;- remote OCI and Git sources are supported.
-
FA9 [sourced] "Services without a
profilesattribute are always enabled";--profile "*"enables every profile. (Profiles) -
FA10 [sourced] Service options:
depends_ontakesservice_healthy;env_filewithrequired: falseis silently ignored when missing;pull_policy: build;extends;- images can be pinned
@<digest>.
-
FA11 [sourced]
develop.watchoffers sync, rebuild and sync+restart, from Compose 2.22.0. (File watch) -
FA12 [sourced]
compose.override.yamlis loaded automatically, and the docs recommendincludefor monorepos. (Merge) -
FA13 [sourced]
docker compose publish --resolve-image-digestscan't publish bind mounts or services that only build. Git include URLs accept#ref:path. (OCI artifact docs; compose #13362) -
FA14 [sourced]
down -vremoves volumes,run --rmremoves the one-off container, andup --waitwaits until services are running or healthy. (Compose CLI reference @ v5.6.0) -
FA15 [sourced] Tilt drives Compose from a Tiltfile with
docker_compose(), adding a UI andlive_update, and is a separate CLI (v0.37.8). (Tilt + Compose) -
FA16 [sourced] GHCR: "You can also access public container images anonymously". A package's first publish is private by default. Add the
org.opencontainers.image.sourcelabel. (Container registry) -
FA17 [sourced] Public packages are free. (Packages billing)
-
FA18 [experiment] Mid-run, Docker Desktop's daemon hung, the CLI crashed with SIGBUS, and dmesg showed disk I/O errors. After the reboot the engine was 29.8.2.
-
FA19 [experiment] An anonymous GHCR token plus the manifest returned 200 (401 without the token), and
docker pull ghcr.io/stefanprodan/podinfo@sha256:cab90e04…worked with an empty credential store. -
FA20 [experiment] An included app fragment plus a platform wiring file, combined in one include
pathlist, resolved. The fragment'sapigotdepends_on: postgres: service_healthy, althoughpostgresis defined by the platform. -
FA21 [experiment] A missing
${BACKEND_SESSION_KEY:?…}failed with "required variable BACKEND_SESSION_KEY is missing a value". An include'senv_filedoesn't leak into the parent's interpolation. -
FA22 [experiment] A service defined in both the platform and an include merged silently: the platform's
imagewon, and the fragment's other keys stayed. Two includes defining the same service also merged silently, last one winning. The code path is compose-go v2.16.1loader/include.go:228-275, which callsMergeYaml(Compose v5.6.0go.mod:10). -
FA22b [experiment]
include: - https://github.com/docker/awesome-compose.git#30f4b7f…:nginx-golang/compose.yamlresolved in 3.0 s without a prompt and was cached under$XDG_CACHE_HOME/docker-compose/. -
FA23 [experiment]
up -d --waittook 14.2 s, andapistarted only afterpostgresreported healthy. -
FA24 [experiment] The Postgres password, delivered as a Compose secret through
POSTGRES_PASSWORD_FILE, logged in. The fragment's hardening applied: non-root, read-only, all capabilities dropped, no-new-privileges, init. -
FA25 [experiment]
--profile observabilityaddedotel; without the profile, it didn't start. -
FA26 [experiment] With
-f compose.local.yaml, the backend built locally as uid 65532, kept its hardening and became healthy (39 s). Without the override, it went back to the pinned digest. -
FA27 [experiment]
develop.watchrebuilt the image on a source change. It first failed with "cannot take exclusive lock … /run/user/1000", becauseXDG_RUNTIME_DIRpointed at a missing directory; setting a writable one fixed it. -
FA28 [inference] From FA16-FA26: pinned images by default plus local overrides gives reproducibility and a fast inner loop.
-
FA29 [inference] From FA1, FA3, FA8 and FA20-FA22b: a fragment holding only the app's own services, with
${VAR:?}names, keeps what the app needs with the app and keeps the provisioning with the platform. Because merges are silent, use prefixed service names and a CI check.
RQ-2: Commands, bootstrap, secrets (FA)
-
FA30 [sourced] Apple ships GNU Make 3.81 (apple-oss-distributions/gnumake, tag gnumake-136) through
xcode-select --install(TN2339). -
FA31 [experiment] From package manifests: the Ubuntu 24.04 WSL image has no
makebut hasgitandcurl. This machine's Ubuntu 22.04 has GNU Make 4.3. -
FA32 [sourced] Installers:
- just: apt, winget, scoop and an
install.shscript (just 1.58.0; re-checked by the lead); - Task: brew, snap, npm, winget (Task install v3.54.0);
- mise: needs
mise trust(mise trust).
- just: apt, winget, scoop and an
-
FA33 [experiment] Make, just, Task and mise all dry-ran
upidentically.Runner statusBinary Friction Make 0.11 s — none just 0.11 s 6.1 MB Docker {{ }}templates need escapingTask 0.35 s 50 MB arguments need a --mise 0.48 s 132 MB statusfailed until wrapped in{% raw %}, and neededmise trust -
FA34 [inference] From FA30-FA33: every runner needs an install somewhere. Make is chosen for familiarity and zero template friction, with scripts holding the logic.
-
FA35 [sourced] mkcert v1.4.4 (2022-04-26) covers the system stores, Firefox (macOS and Linux) and Java, and its README warns that
rootCA-key.pem"gives complete power to intercept secure requests". (mkcert; re-checked by the lead) -
FA36 [sourced] mkcert @
1c1dc4eper OS:- macOS:
sudo security add-trusted-cert -d -k /Library/Keychains/System.keychain(truststore_darwin.go:53); - Linux:
sudo teeplusupdate-ca-certificates(truststore_linux.go:36-73); - NSS:
~/.pki/nssdb(truststore_nss.go:21-24); - Windows:
CertOpenSystemStoreW(0,"ROOT")(truststore_windows.go:72-76).
No code touches hosts files.
- macOS:
-
FA37 [sourced] Windows trust stores:
CertOpenSystemStorereaches "Only current user certificates" (Microsoft Learn);Import-Certificate -CertStoreLocation Cert:\CurrentUser\Root(Import-Certificate);certutil -usertargets the user store (certutil).
-
FA38 [sourced] On Windows,
-installshows a security-warning dialog, and the maintainer wrote "I don't know of any way to avoid the warning". (mkcert #286) -
FA39 [sourced] Chrome on Windows uses the Current User Trusted Root store; on macOS it uses the default and System keychains. (Chrome Root Store FAQ)
-
FA40 [sourced] Firefox:
security.enterprise_roots.enableddefaults to true, and on Windows Firefox readsCERT_SYSTEM_STORE_CURRENT_USER(EnterpriseRoots.cpp:134-217, mozilla-firefox maincfd140a311);- on macOS it imports the system keychain;
- on Linux it does no OS import (AddRootToFirefox).
-
FA41 [sourced] "Since M146, Chromium defaults to $HOME/.local/share/pki/nssdb" (Linux cert management). mkcert misses this path (mkcert #671, open; re-checked by the lead).
-
FA42 [sourced]
security add-trusted-cert: admin trust settings need root. (security.1) -
FA43 [sourced] WSL interop: Windows tools run as the active Windows user.
generateHosts,localhostForwardinganddnsTunneling(Windows 11 22H2+) default to true. (WSL filesystems; wsl-config; networking) -
FA44 [experiment] Read-only probes:
powershell.exe,certutil.exe,wslpathandcmd.exeare present;mkcert.exeis absent;- PowerShell 5.1.26100.9549; WSLInterop is enabled; the user is not elevated;
- Chrome is installed and Firefox is not; Windows build 10.0.26200.
-
FA45 [experiment] A throwaway CA in WSL was readable by
certutil.exe -dumpand PowerShellX509Certificate2through its\\wsl.localhostpath. It was not present inCurrentUser\Root, and nothing was installed. -
FA46 [experiment] Existing state:
- the WSL mkcert CA (SHA-1
88:3B:08…) is in neither Windows store; CurrentUser\Rootholds an mkcert leaf (valid 2025-12-13 to 2028-03-13).
- the WSL mkcert CA (SHA-1
-
FA47 [experiment] Hosts files:
- the Windows hosts ACL is Users read/execute only;
- the file has 7
rsl-commercelines and nodesign.; - WSL resolves
api.through 10.255.255.254 (DNS tunneling), but notdesign..
-
FA48 [sourced] DDEV v1.25.4 manages the Windows hosts file from WSL2 with
ddev-hostname.exe, which self-elevates with the verbrunas(cmd/ddev-hostname/elevate_windows.go:41-80). It can be opted out withWSL2NoWindowsHostsMgt. (ddev) -
FA49 [sourced]
Start-Process -Verb RunAsruns as administrator. (Start-Process) -
FA50 [inference] From FA36 and FA43-FA49: hosts edits always need admin. One opt-in, idempotent, elevated command covers them, and on WSL2 the Windows edit also serves WSL.
-
FA51 [inference] From FA37-FA45: on WSL2, importing the public CA into
CurrentUser\Rootneeds no admin and nomkcert.exe; the only unavoidable step is the dialog. -
FA52 [verified-code] The platform hooks prohibit AWS keys, PEM private keys and GitHub tokens (
platform/.githooks/setup:16-19), and.gitignorecovers only.agent/(platform/.gitignore:1-2), @5db9dab. -
FA53 [sourced] git-secrets checks staged files against prohibited patterns, and supports
--addand--allowed. (awslabs/git-secrets) -
FA54 [experiment]
gen-env.sh:- re-runs were byte-identical;
- after a key was deleted, only that key was regenerated;
- the file mode is 600 and values are never printed;
git check-ignorematched.envandcerts/.
-
FA55 [experiment] With the current patterns, git-secrets let random hex through and blocked a PEM key. An added
AUTHORIZE_NET_TRANSACTION_KEY=[A-Za-z0-9]{16}pattern blocked a filled value and passed an empty one. -
FA56 [sourced] Compose secrets mount at
/run/secrets/<name>. (Secrets in Compose) -
FA57 [sourced] Authorize.Net sandbox:
- free, separate credentials, "No actual card processing";
- Accept Hosted needs
merchantAuthentication.
-
FA58 [sourced] Webhooks are signed with HMAC-SHA512 in
X-ANET-Signature, and the endpoint must be HTTPS. (Webhooks) -
FA59 [inference] From FA1 and FA57-FA58: credentials can't be generated, so they are optional inputs, and webhooks need reconciliation locally.
-
FA60 [experiment] An idempotent seed run twice left a count of 1. Data survived
down/up.run --rmwaited for Postgres to be healthy.down -vremoved the volume. -
FA61 [inference] From FA4, FA14 and FA60: the platform orchestrates seed and reset, and the backend owns their content.
RQ-3: Proxy and TLS (FB)
-
FB7 [sourced] Latest releases: Traefik v3.7.14, Caddy v2.11.7, nginx 1.31.6 (mainline) and 1.30.5 (stable). (GitHub releases API)
-
FB8 [sourced] Compressed image sizes: Traefik 52.8 MB, Caddy 23.7 MB, nginx-unprivileged alpine-slim 7.9 MB. (Docker Hub v2 API)
-
FB9 [sourced] Traefik's docs on the Docker socket: "If Traefik is attacked, then the attacker might get access to the underlying host".
exposedByDefaultis true. (Traefik Docker provider) -
FB10 [sourced] "Custom headers will overwrite existing headers if they have identical names". (Traefik headers)
-
FB12 [sourced] Caddy proxies WebSocket automatically. (reverse_proxy)
-
FB13 [sourced] nginx needs explicit
UpgradeandConnectionheaders for WebSocket. (nginx WebSocket) -
FB14 [sourced] Docker's default sysctls let processes bind ports below 1024 without capabilities. (moby#41030)
-
FB15 [sourced] The Caddy image runs
setcap cap_net_bind_service=+epand has noUSER(caddy-docker 2.11/alpine/Dockerfile:39). The Traefik image has noUSEReither. -
FB16 [sourced] If a binary's file capabilities are masked, "execve(2) fails with the error EPERM". (capabilities(7))
-
FB17 [experiment] Caddy under
cap_drop: ALLfails with "exec caddy failed: Operation not permitted". It works withcap_add NET_BIND_SERVICE, or with a derived image aftersetcap -r. -
FB18 [experiment] Traefik, Caddy and nginx each served all 8 hosts with 200 over HTTP/2, added HSTS, passed the backend's CSP through unchanged, and kept
/api/queues/%2F/q1intact. For an unknown host, Traefik served its default certificate and a 404, Caddy sent a TLS alert, and nginx sent "unrecognized name". -
FB19 [experiment] All three returned
101 Switching Protocolswith a correctSec-WebSocket-Accept, and echoed a frame. -
FB20 [experiment] Idle RAM and users:
Proxy Idle RAM User Capabilities Traefik 39.3 MiB 65534 none Caddy 15.0 MiB 65534 needs NET_BIND_SERVICEnginx 20.6 MiB 101 none -
FB21 [sourced] GitHub security advisories in 2026: Traefik 58 (2 critical, 32 high), Caddy 17. (
gh api …/security-advisories) -
FB22 [sourced] nginx: 21 advisories in 2026; the latest (CVE-2026-90439) is fixed in 1.31.6 and 1.30.5. (nginx advisories)
-
FB23 [inference] From FA1, FB9 and FB17-FB22: routing is static and owned by the platform. nginx meets DE-5 with no exceptions and reuses the org's existing image.
-
FB24 [sourced] mkcert's issues asking for name constraints (#302, #377) are still open. (mkcert)
-
FB25 [experiment] mkcert 1.4.4, with CAROOT in scratch and never
-install, made a 10-year root withpathlen:0and no name constraints, and an 825-day leaf with 8 SANs. -
FB26 [experiment] A leaf for
rsl-commerce.testplus*.rsl-commerce.testmatched the bare domain,api.anddesign., but nota.b.. -
FB27 [sourced] A wildcard matches only the leftmost label. (RFC 6125 §6.4.3)
-
FB28 [experiment] With a root carrying
nameConstraints=critical,permitted;DNS:rsl-commerce.test, the 8-SAN leaf verified. Awww.example.comleaf from the same root failedopenssl verifyand curl with "permitted subtree violation". -
FB29 [sourced] Caddy's local CA: a 10-year root and a 7-day intermediate, with
skip_install_trustandcaddy trust. (Automatic HTTPS) -
FB30 [experiment] Caddy
tls internalproduced a root valid to 2036 withpathlen:1and no name constraints, and 12-hour leaves. -
FB31 [sourced] step-ca is an online ACME CA. (step-ca)
-
FB32 [sourced]
.testis reserved, and registrars "MUST NOT grant" it. (RFC 6761 §6.2) -
FB33 [sourced] The Chromium HSTS preload list has no
testentries. (transport_security_state_static.json) -
FB34 [sourced] On HSTS errors the browser "MUST terminate the connection" with "no user recourse", and
max-age=0clears the policy. (RFC 6797) -
FB35 [sourced]
.testis not a potentially trustworthy origin by name. (Secure Contexts) -
FB36 [inference] From FB24-FB35: a name-constrained root limits the damage if its key leaks, explicit SANs keep the certificate equal to the host list, and a short local HSTS avoids a lock-in that can't be bypassed.
RQ-4: Services (FB)
- FB37 [verified-code] The platform provides PostgreSQL, Redis, RabbitMQ, Mailpit and Meilisearch (
platform/README.md:13 @ 5db9dab). Frontends never talk to them (AGENTS.md:23 @ d0573a0). - FB38 [verified-code] "a second database or message broker without a reason" is ruled out. (
README.md:59-61 @ d0573a0) - FB39 [sourced] postgres 18.6: PGDATA is
/var/lib/postgresql/18/dockerand the VOLUME is/var/lib/postgresql(Docker Hub postgres). It is supported to 2030-11-14 (Versioning). Licence: the PostgreSQL Licence. - FB40 [experiment] All six services ran as non-root, with a read-only root filesystem, all capabilities dropped, no-new-privileges and init, and all were healthy. Idle RAM: PostgreSQL 46.1, Valkey 7.0, Redis 8.9, RabbitMQ 170.7, Mailpit 10.6, Meilisearch 38.0 MiB.
- FB41 [experiment] Mailpit and Meilisearch running as 65534 on a fresh volume both got "permission denied", until derived images ran
chownon the mount point. - FB42 [experiment] Behaviour checks:
- a queue declared without a type came back
quorum; - Mailpit received an SMTP message;
- Meilisearch
/healthreported available, and other routes returned 401 without the key; - the PostgreSQL 18.6 data directory was confirmed.
- a queue declared without a type came back
- FB43 [sourced] Redis 8.0+ is "RSALv2, SSPLv1, and AGPLv3", 7.4 is RSALv2/SSPLv1, and 7.2 and earlier are BSD-3. (Redis licenses; re-read by the lead)
- FB44 [sourced] Valkey is BSD-3, under the Linux Foundation, with 9.1.2 the latest (valkey.io; re-checked by the lead). It is not a Docker Official Image; the image comes from
valkey-io/valkey-container. - FB45 [sourced] go-redis aims "to support the last three releases of Redis" and doesn't mention Valkey (go-redis v9.23.0). valkey-go v1.0.78 has a go-redis-style adapter (valkey-go).
- FB46 [experiment] go-redis v9.23.0 on Go 1.27.1 passed the same operations on both Valkey 9.1.2 (reporting
redis_version:7.2.4) and Redis 8.10.2: RESP3 HELLO, SET EX/GET, MULTI INCR+EXPIRE, SET NX, EVALSHA unlock and CLIENT SETINFO. - FB47 [sourced] ElastiCache supports Valkey 7.2 through 9.1 and "all Redis OSS versions 7.1 and before" (ElastiCache engine versions; re-read by the lead). Valkey is priced 20-33% lower (What is Valkey). Memorystore supports Valkey 7.2-9.1 (Memorystore for Valkey).
- FB48 [sourced] Started as root, the Valkey entrypoint runs
chownand thensetpriv; it also accepts--user. (docker-entrypoint.sh) - FB49 [sourced] The RabbitMQ 4.3.6 images have no default healthcheck (Docker Hub rabbitmq). On
default_queue_type = quorum, the vhost setting wins over the node setting (Virtual hosts). - FB50 [sourced] The quorum-queue delivery limit defaults to 20. Messages are dropped without a DLX, and at-least-once dead-lettering needs
reject-publish. (Quorum queues) - FB51 [sourced] Mailpit uses ports 1025 and 8025,
MP_DATABASEand a/datavolume, under the MIT licence. (Mailpit Docker) - FB52 [sourced] Meilisearch is "MIT AND BUSL-1.1": the community image is MIT, and Enterprise is a separate image. Telemetry is on unless disabled. (meilisearch v1.54.3)
- FB53 [inference] From FB43-FB47 and FB40: Valkey fits the licence, the managed production services and RAM, and the probe shows client compatibility.
RQ-5: Observability (FB)
- FB54 [verified-code] Local logs, metrics, traces and dashboards (
platform/README.md:14 @ 5db9dab); "Observable systems" (README.md:53 @ d0573a0). - FB55 [sourced] otel-lgtm is "intended for development, demo, and testing environments". It persists to
/dataand provisions dashboards from a custom directory. (docker-otel-lgtm v0.35.0, Apache-2.0; re-checked by the lead) - FB56 [experiment] lgtm v0.35.0 bundles otelcol 0.161.0, Prometheus 3.15.0, Tempo 3.0.3, Loki 3.7.8 and Grafana 13.2.2, and runs as root.
- It was healthy within 30 s, using 529 MiB at start and 600 MiB idle after ingest.
- Traces, logs (including a curl OTLP/HTTP log) and a metric all arrived.
- The image is 3.67 GB.
- FB57 [experiment] Run as 65534 with a read-only root and all capabilities dropped, lgtm needed tmpfs on
/tmp,/dataand/var/tempo. It was then healthy at 552 MiB. Grafana logged non-fatal errors about installing plugins on a read-only filesystem. - FB58 [experiment] The separate stack totalled about 485 MiB (Grafana 312, Loki 49, otelcol 40, Prometheus 42, Tempo 42), and all three signals arrived. Every image is non-root.
- FB59 [sourced] SigNoz needs "At least 4GB of memory" and runs ClickHouse, Keeper and Postgres. (SigNoz Docker)
- FB60 [sourced] Jaeger v2 is "primarily the tracing backend". (Jaeger)
- FB61 [sourced] "Promtail is end of life (EOL) as of March 2, 2026". (Promtail)
- FB62 [sourced] The Loki Docker driver is installed per host, isn't supported on Windows, and can block. (Docker driver)
- FB63 [sourced] The filelog container parser takes only Kubernetes metadata from file paths. (filelogreceiver)
- FB64 [experiment] Docker Desktop uses the
locallogging driver, and a bind mount of/var/lib/docker/containerscame up empty. That probe also left an empty, root-owned/var/lib/docker/containersdirectory in the WSL distro (confirmed by the lead). - FB65 [sourced] Alloy's
loki.source.dockerreads through/var/run/docker.sock. (loki.source.docker) - FB66 [sourced] Grafana provisions datasources and dashboards from files using
apiVersion: 1. (Provisioning) - FB67 [sourced] OTel Go v1.47.0 (2026-10-02) is "the first stable release of the OpenTelemetry Go Logs API and SDK". (opentelemetry-go v1.47.0; re-checked by the lead)
- FB68 [sourced] OTel JS logs are still at "Development". (opentelemetry-js v2.12.0)
- FB69 [sourced]
OTEL_EXPORTER_OTLP_ENDPOINTandOTEL_EXPORTER_OTLP_PROTOCOL(defaulthttp/protobuf), on ports 4317 and 4318. (OTLP exporter spec) - FB70 [inference] From FB55-FB69: lgtm covers all four signals with the fewest moving parts. File-based log collection isn't possible here, and socket-based collection is root-equivalent, so OTLP plus
compose logsis the DE-5-safe default. - FB71 [experiment] After the crash, the Tempo and otelcol binaries were exactly 104,857,600 and 406,847,488 bytes, truncated, and both segfaulted. Re-pulled, they were 109,273,250 and 408,858,786 bytes and ran.
RQ-6: Tests, matrix, CI (FC)
-
FC1 [verified-code] Health checks, ecosystem tests and the version record (
platform/README.md:16-18 @ 5db9dab). -
FC2 [verified-code] "Running the platform must never require a real secret". (
platform/AGENTS.md:18 @ 5db9dab) -
FC4 [sourced] Checkout D1 and D3: Accept Hosted by redirect, with
form-action https://test.authorize.net. (commerce-checkout#1) -
FC6 [sourced] Test cards 4007000000027 and 4111111111111111, and ZIP 46282 for a decline. (Testing guide)
-
FC7 [sourced] There is no maintained Authorize.Net mock: the candidates were archived or last pushed between 2011 and 2022. (
gh api search/repositories) -
FC8 [sourced]
page.route()withroute.fulfill. (Playwright Network 1.63) -
FC9 [experiment] Chromium 153 with
--host-resolver-rules=MAP *.rsl-commerce.test 127.0.0.1loadedapi.andcheckout.with 0 lines in the hosts file. With the self-signed certificate it failed withERR_CERT_AUTHORITY_INVALID; withignoreHTTPSErrorsit reportedisSecureContext: trueand stored a__Host-cookie. -
FC10 [sourced]
docker compose config -qvalidates only. (compose config) -
FC11 [sourced] Schemathesis 4.29.4 (MIT) supports OpenAPI 2.0 through 3.2, stateful testing through links, and
--report junit. (PyPI; docs; re-checked by the lead) -
FC12 [experiment] Schemathesis against a stub with 2 planted bugs found the schema violation and the TRACE cases at
--max-examples 50, but missed the 500. At 200 it found all 5 unique failures in 30.7 s. -
FC13 [sourced] Dredd is archived (last pushed 2024-05-11; re-checked by the lead), and its OpenAPI 3 support is "experimental". (apiaryio/dredd)
-
FC14 [sourced] Prism v5.16.0 has a validation proxy with
--errors. (stoplightio/prism) -
FC15 [experiment] The Prism proxy flagged the planted response bug, and
npm auditfound 15 issues (6 moderate, 9 high). -
FC16 [sourced] Dependabot:
- the Docker Compose ecosystem gets version updates but no security updates;
- cooldown is supported, and
directoriesaccepts globs (ecosystems; options); - GHCR has no publication dates for cooldown (dependabot-core
docker/README.md).
-
FC17 [sourced] dependabot-core:
- its specs update the tag and digest together (
docker/spec/dependabot/docker_compose/file_updater_spec.rb:376,411); FILENAME_REGEX = /(docker-)?compose(-[\w]+)?(?>\.[\w-]+)?\.ya?ml/i(docker_compose/file_fetcher.rb:9);- only files directly in the configured directory are read (
:31).
- its specs update the tag and digest together (
-
FC18 [sourced] Cross-repo dispatch needs a PAT or an App with Contents write;
GITHUB_TOKENis limited to its own repository. (repository dispatch; GITHUB_TOKEN) -
FC19 [sourced] Standard public runners have 4 CPUs, 16 GB of RAM and 14 GB of SSD, free; larger runners are always billed. (hosted runners; re-read by the lead; Actions billing)
-
FC20 [experiment] Compressed sizes from
docker manifest inspect, in MB:Image MB Playwright 955.8 otel-lgtm 915.3 postgres alpine 120.0 meilisearch 106.0 rabbitmq alpine 89.0 valkey alpine 17.5 mailpit 16.8 nginx-unprivileged 8.3 -
FC21 [inference] From FC19-FC20: about 2.4 GB compressed fits, and leaving observability out of per-PR runs saves headroom. RAM is unmeasured.
-
FC22 [verified-code] CodeQL runs
[actions]only, with tag pins at lines 27, 30 and 36. Dependabot covers only github-actions. (platform/.github/workflows/codeql.yml:24;dependabot.yml:4-8@5db9dab) -
FC23 [sourced] The DE-4 lists (commerce#3):
- adopted: hadolint, Grype, SBOM with attestations, dependency-review;
- rejected: zizmor and Scorecard as CI jobs.
-
FC24 [sourced] Current tool versions: hadolint v2.15.1, grype v0.120.1, scan-action v7.4.2 and shellcheck v0.11.0. CodeQL's Actions analysis has been GA since 2025-04-22. (
gh apireleases) -
FC25 [inference] From FC2-FC9 and FC18: a fake gateway plus the browser intercept needs no secrets and doesn't loosen checkout's CSP. The sandbox job must never be a required check.
RQ-7: Engineering docs site (FC)
-
FC26 [verified-code] The
docs.row still says "Design system and engineering documentation" (README.md:26 @ d0573a0), but P-D0 and DE-11 split the hosts. -
FC27 [sourced] DS-D6 uses VitePress 2, and its docs CSP needs 2 script hashes plus
'unsafe-inline'styles. (design-system#1) -
FC28 [sourced] Alternatives:
- Starlight 0.42.5 uses Pagefind;
- Astro's CSP on static sites is meta-only, and external styles aren't supported out of the box (Astro config);
- Docusaurus 3.10.2 offers first-class Algolia search, while local search comes from the community (Docusaurus search);
- TechDocs needs MkDocs plus a backend (TechDocs).
-
FC29 [experiment] VitePress 2.0.0-alpha.20 emits two fixed inline scripts. With their hashes plus inline styles, the CSP showed 0 violations. On nginx-unprivileged running read-only with all capabilities dropped (uid 101),
/and/api/scalarreturned 200 and/nope404. -
FC30 [experiment] Scalar 1.73.0, as a Vue component inside
<ClientOnly>, rendered. Under the VitePress policy its one violation was anevalprobe. -
FC31 [experiment] Scalar defaults to fonts and telemetry on (
api-reference-configuration.js:409;base-configuration.js:163), which gave 14font-srcviolations to fonts.scalar.com. The eval violation is Zod'sFunctionprobe, which falls back safely. -
FC32 [experiment] VitePress merged Scalar's CSS into its single global stylesheet (368 kB), which every page loads.
-
FC33 [experiment] Swagger UI 5.33.1 had 0 violations under every policy, including the one without
unsafe-inline; "Try it out" and "Execute" were exercised. -
FC34 [experiment] Redoc 2.5.4 had 10
style-src-elemviolations and crashed under strict styles. With inline styles it rendered, but its search worker and CDN logo still caused violations. -
FC35 [experiment] Elements 9.0.27 under strict styles had 1 violation and a broken layout; with inline styles it was clean.
-
FC37 [sourced] Renderer facts, re-checked by the lead with
npm view:Renderer Version Licence Notes Scalar 1.73.0 MIT OpenAPI 3.1 and 3.2 Redoc 2.5.4 MIT — Swagger UI 5.33.1 Apache-2.0 open CSP issues #5817 and #10655 Elements 9.0.27 Apache-2.0 open issue #2506 -
FC38 [experiment]
npm auditof the site found 22 issues (6 high, via Elements). Direct dependencies: Scalar 27, Redoc 21, Elements 12, swagger-ui-dist 1. Scalar unpacks to 46.7 MB. -
FC39 [sourced] Mermaid in VitePress:
vitepress-plugin-mermaid2.0.17 peersvitepress ^1andmermaid 10 || 11(re-checked by the lead), and its VitePress 2 issue #94 is open;- Mermaid's C4 support is "experimental";
- Structurizr Lite is end of life.
-
FC40 [experiment] Mermaid 12.1.0:
- rendered at runtime under the strict policy: 116 violations and an unreadable chart;
- rendered at runtime under the VitePress policy: 0 violations, but +0.65 MB gzip of JS;
- pre-rendered to SVG and embedded with
<img>: 0 violations and +224 B.
-
FC41 [sourced] VitePress local search uses minisearch, and markdown supports
@include. (Search; Markdown) -
FC42 [sourced] GitHub Pages: 1 GB per site, 100 GB/month soft bandwidth, and no e-commerce (limits). Custom headers aren't supported, so a CSP needs a meta tag (community discussion #54257).
-
FC43 [sourced] The contents API returns raw files (contents).
GITHUB_TOKENallows 1,000 requests per hour (rate limits). Release assets can be up to 2 GiB (releases). -
FC44 [verified-code] The submodules are relative (
.gitmodules:1-21 @ d0573a0). The only ADRs so far are inagent-workflow-tooling/docs/adr/(0001-…md:1,0002-…md:1@c1d006c). -
FC45 [inference] From FC43-FC44: fetching at build time at the matrix SHAs is reproducible, but makes
docs/adr/a convention. Submodules would couple platform to the parent repo. The spec belongs as a release asset (D2, DE-7). -
FC46 [inference] From FC29-FC38: Swagger UI is the only renderer clean under the full strict policy, and it is smaller and has one dependency.
Experiments
- E1, orchestration and bootstrap (
pf-a)- Runs:
- GHCR anonymous token and pull;
docker-composev5.6.0configfor the include, required-variable and conflict tests;up -d --wait, theobservabilityprofile,-f compose.local.yamlandwatch;- Postgres secret login;
gen-env.shthree times, andgit secrets --scan;make -n,just --dry-run,task --dryandmise run --dry-run;- read-only probes:
command -v,$PSVersionTable,wslpath -w,icacls.exe, aCert:\CurrentUser\Rootcount, andcertutil.exe -dump <UNC>; - a Git remote include;
- seed and reset.
- Results: anonymous pull, required variables, the remote include, the health gate, secret delivery, profiles, the local override,
watch(onceXDG_RUNTIME_DIRwas fixed), idempotent.env, and seed and reset all passed. Conflicting service definitions merge silently. git-secrets doesn't catch random hex. - Cleanup: containers, networks, volumes and images were removed.
- Runs:
- E2, proxy, TLS, services and observability (
pf-b, 11:45-12:05 after the reboot)- Runs:
- the mkcert binary, with CAROOT in scratch and never
-install; - an
opensslname-constrained root and leaves, checked withopenssl verifyandcurl --cacert --resolve; - Caddy capability tests;
- a Compose project with web, echo, Traefik, Caddy,
caddy-internaland nginx, tested withtest-proxy.shandws-test.mjs; - the services project, using the RabbitMQ API, SMTP, Meilisearch and
psql; - a go-redis probe on Go 1.27.1;
- otel-lgtm with telemetrygen and curl OTLP, plus Tempo, Loki and Prometheus queries;
- the separate observability stack;
docker infoand the log-mount probe.
- the mkcert binary, with CAROOT in scratch and never
- Cleanup: every container, network and volume was removed, the throwaway keys were deleted, and the pulled images remain (about 8 GB).
- Runs:
- E3, tests and docs (
pf-c, redone after the reboot)- Runs:
- a VitePress 2.0.0-alpha.20 site with Scalar, Redoc, Swagger UI, Elements and Mermaid, built with
npx vitepress build docs; serve.mjsserving three CSP variants, plus nginx-unprivileged running read-only with all capabilities dropped;- a Playwright probe recording
securitypolicyviolationevents, errors and transfer sizes; - Schemathesis 4.29.4 against a stub on
e3-net; - the Prism proxy;
- a host-resolver and TLS check with a throwaway certificate.
- a VitePress 2.0.0-alpha.20 site with Scalar, Redoc, Swagger UI, Elements and Mermaid, built with
- Cleanup: containers and the network were removed.
- Runs:
- Lead checks (after all runs):
- the hosts-file checksums and certificate counts are unchanged from the pre-experiment baseline;
docker ps -ais empty.
workflow · claude-code · 2026-10-07 12:13
-
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Choose the platform stack: orchestration, commands and bootstrap, reverse proxy and local TLS, infrastructure services, observability, ecosystem tests and the compatibility matrix, and the engineering docs site. Part of the tech stack epic in commerce.
Status
Appetite: 6h active (Default) · Estimate: — · Active so far: see the
timecommentBrief
Idea: "Now, let's plan the final child: platform. This is different from the others, but it should have the documents site."
Problem and audience: The platform is how anyone runs the whole commerce system on their own machine. It makes the storefront, admin, checkout, API, docs, mail and observability reachable at local HTTPS addresses. It ships no application, so its "stack" is orchestration, a command interface, TLS and routing, infrastructure services, observability, tests across the ecosystem, and now the engineering docs site. Its audience is contributors, and anyone cloning the portfolio, on WSL2, macOS or Linux.
Outcome: A recorded choice for each of those areas that keeps the boundary: applications declare what they need, and the platform provisions it. A new contributor reaches a working environment with as few manual steps as possible.
Scope.
docs.rsl-commerce.test.design.host.Constraints:
README.md:10-18).README.md:20-32).AGENTS.md:8-12).AGENTS.md:16-19).../README.md:59-61).Known in code: None. The repo is docs-only (
5db9dab).Track: full — the options need outside sources and experiments · Appetite: 6h active (Default)
include, health-gateddepends_on,develop.watch) or Tilt? How does each app join: pinned GHCR images, sibling builds, or both with a local override? How does each app declare its infrastructure needs and config?AGENTS.md:8-10• The command runner: Make, just, Taskfile, mise or a small CLI.
• Cross-OS bootstrap: the CA trusted on Windows from inside WSL2, on macOS and on Linux; hosts-file lines.
• Generated dev secrets.
• Optional Authorize.Net sandbox credentials.
• What
seedandresetmean..testcaveats.observe.The OTel Collector withgrafana/otel-lgtm, or separate Prometheus, Loki, Tempo and Grafana, or SigNoz, or Jaeger with Prometheus. Covers Go and Node telemetry, container logs, dashboards as code, and RAM.../README.md:49)• Playwright E2E across storefront → checkout → backend, with a payment stub when no credentials are set.
• Smoke and OpenAPI contract tests.
• A matrix of image digests verified in CI.
• Dependabot for compose.
• DE-4 checks for this repo.
README.md:17-18)docs.• Tool: VitePress 2, Starlight, Docusaurus or TechDocs.
• Gathering ADRs and READMEs from every repo without reaching around contracts.
• An API reference from the backend's published OpenAPI spec under a strict CSP: Scalar, Redoc, Swagger UI or Elements.
• Diagrams, search, serving, and public publishing (off until asked).
Assumptions:
docs.rsl-commerce.testfor engineering here,design.rsl-commerce.testfor the design system (Decided, @ABilenduke, 2026-10-07).Spec
Not written yet.
Plan
Not written yet.
Decisions
docs.rsl-commerce.testfor the engineering docs (this repo) anddesign.rsl-commerce.testfor the design-system docs, making eight local hosts.observabilityandtest.up --waitalways.• Contract: each app repo ships
compose.platform.yaml. It covers the app's own services only, with prefixed names, env names as${VAR:?}, a healthcheck and DE-5 hardening. It publishes no ports and names no infra hosts, and it sets noimage:.• Wiring and pins: the platform's wiring files
compose/compose.<app>.yamlhold the literalname:tag@sha256image pins (the matrix) and thedepends_on.• Fetching the fragment: a Git remote include at the app's release ref.
• Local work: an opt-in build from the sibling checkout (
LOCAL=<app>).shscripts:bootstrap,up,down,reset,seed,logs,status,doctor,hosts,trust.opensslcreates a root name-constrained torsl-commerce.test(pathlen:0, ≤ 825 days) and one leaf with the 8 SANs. No mkcert.Trust uses each OS's own tools:
• WSL2:
Import-CertificateintoCurrentUser\Root(no admin);• macOS:
security add-trusted-certplus NSS;• Linux: system store plus both NSS paths.
Hosts lines are printed, and
make hostsapplies a marked block with one elevation.•
gen-env.shgenerates per-install secrets into a gitignored, mode-600.env;.env.exampleis committed.• The Postgres password is a Compose secret.
• git-secrets gets key-name and Authorize.Net patterns.
• Authorize.Net sandbox credentials are optional, and without them payments are disabled.
ssl_reject_handshakeas the default, and a WebSocket map onapi.. The proxy sets TLS, HSTS (max-age=86400; includeSubDomains, local only) andX-Forwarded-*. The apps set every other security header.default_queue_type = quorum; UI on127.0.0.1:15672only), Mailpit 1.31 and Meilisearch 1.54 (community MIT build; analytics off).All run non-root and read-only with every capability dropped. Mailpit and Meilisearch use 2-line derived images.
grafana/otel-lgtm0.35, pinned, hardened (uid 65534, read-only, tmpfs mounts), with Grafana atobserve.and dashboards provisioned from files.Logs: the apps send OTLP, and
make logscovers infra. No Docker-socket log profile.• Tier 1, every PR and nightly, needs no secrets. Playwright runs against the stack, with the backend pointed at the fake Authorize.Net gateway, which the backend repo owns and publishes as a test-only image, and the browser leg intercepted.
• Tier 2, optional, manual or weekly: the real sandbox.
• Smoke:
curl --resolveover every host.• Contract: pinned Schemathesis, with fixed seeds on PRs.
• Matrix: the image pins in
compose/compose.<app>.yaml, kept current by Dependabotdocker-compose(our images grouped with no cooldown, third-party images with a 7-day cooldown). Main is the known-good set.• Fragment refs: bumped together with the images by a small scheduled workflow.
• Workflows:
ci.yml,nightly.yml, a manualsandbox-e2e.ymlandcodeql.yml, on standard runners.• DE-4 for this repo: CodeQL
actionsplusjavascript-typescript; SHA pins; Dependabot for actions, npm, docker and docker-compose;docker compose config; shellcheck; hadolint; Grype; dependency-review.docs.: VitePress 2.0 alpha, pinned exactly, themed from the design-system tokens.• API reference: Swagger UI on its own static page.
• Diagrams: Mermaid pre-rendered to SVG.
• Content: READMEs linked out; an ADR index fetched from
docs/adr/at the matrix SHAs; the OpenAPI spec from the backend's release asset.• Local search, served on nginx-unprivileged. GitHub Pages stays off until the user asks.
ADR: to be written through a PR in this repo.
Follow-ups:
README.md:20-26,platform/README.md:13,22-30anddesign-system/README.md:13for the eight hosts and "Valkey (Redis-compatible)".compose.platform.yaml; a health endpoint; ADRs underdocs/adr/.seed,migrateandhealthchecksubcommands;openapi.jsonas a release asset;Links
design.rsl-commerce.test.