Skip to content

Choose the platform stack #1

Description

@ABilenduke

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

Stage Track Next action Branch PR
done full Write the platform ADR through a PR; track the follow-ups below — —

Appetite: 6h active (Default) · Estimate: — · Active so far: see the time comment

Brief

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.

  • In:
    • orchestration and how apps join the stack;
    • commands, bootstrap, config and secrets;
    • reverse proxy and local TLS;
    • PostgreSQL, Redis, RabbitMQ, Mailpit and Meilisearch;
    • observability;
    • ecosystem tests, the compatibility matrix and CI;
    • the engineering docs site at docs.rsl-commerce.test.
  • Out: production infrastructure; application code, schemas or migrations; the design-system docs app (design-system#1).
  • Deferred: the platform ADR, made through a PR. README updates for the new design. host.

Constraints:

  • Commands: bootstrap, up, down, reset, seed, logs and status. Also the proxy and TLS, the infrastructure, observability, generated config and secrets, health checks, ecosystem tests and the version matrix (README.md:10-18).
  • Local addresses, one hosts-file line each (README.md:20-32).
  • Applications own their contract with infrastructure, and the platform provisions it. No application code. Local is not production (AGENTS.md:8-12).
  • Minimal manual steps; development secrets only, so running the platform never needs a real secret; every service meets a real need (AGENTS.md:16-19).
  • No Kubernetes "for the résumé", and no second database or broker without a reason (../README.md:59-61).
  • From the earlier spikes:
    • the backend is Go with the OTel SDK, workers and WebSocket, and needs quorum queues with a DLX (commerce-backend#1 D1-D6);
    • the storefront is Nuxt SSR on distroless Node; admin, checkout and the design-system docs are static on nginx-unprivileged (commerce#3 DE-5);
    • the proxy terminates TLS and sets HSTS, while the apps set their own CSPs (commerce-checkout#1 D4, commerce-admin#1 D5);
    • checkout uses Authorize.Net sandbox redirects (commerce-checkout#1 D1);
    • CI security per commerce#3 DE-4.

Known in code: None. The repo is docs-only (5db9dab).

Track: full — the options need outside sources and experiments · Appetite: 6h active (Default)

ID Research question Track Why it matters to the plan
RQ-1 Orchestration. Compose v2 (profiles, include, health-gated depends_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? technical The skeleton, and the boundary in AGENTS.md:8-10
RQ-2 Commands and bootstrap.
• 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 seed and reset mean.
technical Onboarding with few manual steps and no real secrets
RQ-3 Proxy and TLS. Traefik v3, Caddy 2 or nginx, with mkcert, Caddy's internal CA or step-ca. Must cover eight hosts, WebSocket upgrades, HSTS, running non-root on 443, and .test caveats. technical Routing and local TLS
RQ-4 Infrastructure services. PostgreSQL 18, RabbitMQ 4.x, Mailpit and Meilisearch, plus Redis 8 or Valkey (licences, go-redis compatibility). Healthchecks, volumes, RAM, and DE-5 fit. technical The services the backend declares
RQ-5 Observability at observe. The OTel Collector with grafana/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. technical Making failures visible (../README.md:49)
RQ-6 Tests, compatibility and CI.
• 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.
technical Integration tests and "known to work together" (README.md:17-18)
RQ-7 Engineering docs site at 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).
technical The docs site the user placed here

Assumptions:

  • Contributors have Docker Engine or Docker Desktop.
  • Observability covers local development only.
  • Bootstrap targets WSL2, macOS and Linux (Decided, @ABilenduke, 2026-10-07).
  • Docs live on separate hosts: docs.rsl-commerce.test for engineering here, design.rsl-commerce.test for the design system (Decided, @ABilenduke, 2026-10-07).

Spec

Not written yet.

Plan

Not written yet.

Decisions

ID Decision Kind Source
P-D0 Separate docs hosts: docs.rsl-commerce.test for the engineering docs (this repo) and design.rsl-commerce.test for the design-system docs, making eight local hosts. Decided @ABilenduke, 2026-10-07
P-D0b Bootstrap supports WSL2 (Docker in WSL; browser, hosts file and trust store on Windows), macOS and Linux. Decided @ABilenduke, 2026-10-07
P-D1 Orchestration: Docker Compose ≥ 5.0 only, with no Tilt. Profiles: none (proxy, infra, apps), observability and test. up --wait always. Decided @ABilenduke, 2026-10-07; research RQ-1
P-D2 How apps join:
• 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 no image:.
• Wiring and pins: the platform's wiring files compose/compose.<app>.yaml hold the literal name:tag@sha256 image pins (the matrix) and the depends_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>).
Decided (the image-pin placement is to be verified at scaffold) @ABilenduke, 2026-10-07; research RQ-1, §1 reconciliation
P-D3 Commands: Make (GNU Make 3.81-compatible) over POSIX sh scripts: bootstrap, up, down, reset, seed, logs, status, doctor, hosts, trust. Decided @ABilenduke, 2026-10-07; research RQ-2
P-D4 Local TLS: openssl creates a root name-constrained to rsl-commerce.test (pathlen:0, ≤ 825 days) and one leaf with the 8 SANs. No mkcert.
Trust uses each OS's own tools:
• WSL2: Import-Certificate into CurrentUser\Root (no admin);
• macOS: security add-trusted-cert plus NSS;
• Linux: system store plus both NSS paths.
Hosts lines are printed, and make hosts applies a marked block with one elevation.
Decided @ABilenduke, 2026-10-07; research RQ-2, RQ-3
P-D5 Config and secrets:
• gen-env.sh generates per-install secrets into a gitignored, mode-600 .env; .env.example is 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.
Decided @ABilenduke, 2026-10-07; research RQ-2
P-D6 Proxy: nginx-unprivileged on the stable alpine-slim line, pinned by digest, with a file config: one server block per host, ssl_reject_handshake as the default, and a WebSocket map on api.. The proxy sets TLS, HSTS (max-age=86400; includeSubDomains, local only) and X-Forwarded-*. The apps set every other security header. Decided @ABilenduke, 2026-10-07; research RQ-3
P-D7 Services: PostgreSQL 18, Valkey 9.1 instead of Redis, RabbitMQ 4.3-management (default_queue_type = quorum; UI on 127.0.0.1:15672 only), 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.
Decided @ABilenduke, 2026-10-07; research RQ-4
P-D8 Observability: grafana/otel-lgtm 0.35, pinned, hardened (uid 65534, read-only, tmpfs mounts), with Grafana at observe. and dashboards provisioned from files.
Logs: the apps send OTLP, and make logs covers infra. No Docker-socket log profile.
Decided @ABilenduke, 2026-10-07; research RQ-5
P-D9 Ecosystem tests:
• 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 --resolve over every host.
• Contract: pinned Schemathesis, with fixed seeds on PRs.
Decided @ABilenduke, 2026-10-07; research RQ-6
P-D10 Matrix and CI:
• Matrix: the image pins in compose/compose.<app>.yaml, kept current by Dependabot docker-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 manual sandbox-e2e.yml and codeql.yml, on standard runners.
• DE-4 for this repo: CodeQL actions plus javascript-typescript; SHA pins; Dependabot for actions, npm, docker and docker-compose; docker compose config; shellcheck; hadolint; Grype; dependency-review.
Decided @ABilenduke, 2026-10-07; research RQ-6
P-D11 Engineering docs at 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.
Decided @ABilenduke, 2026-10-07; research RQ-7

ADR: to be written through a PR in this repo.

Follow-ups:

  • READMEs: update the root README.md:20-26, platform/README.md:13,22-30 and design-system/README.md:13 for the eight hosts and "Valkey (Redis-compatible)".
  • Each app repo: a public GHCR image per release with the source label; compose.platform.yaml; a health endpoint; ADRs under docs/adr/.
  • backend:
    • seed, migrate and healthcheck subcommands;
    • payments disabled when no credentials are set;
    • a configurable gateway base URL and signature key;
    • the fake-gateway test image;
    • openapi.json as a release asset;
    • test identities;
    • OTLP logs.
  • checkout: take the hosted-form URL from the session response.
  • Verify in the first bootstrap: browsers enforce the name constraint, and Windows trust works from WSL.

Links

Activity

  1. added
    type:spikeResearch that answers a question and ends there
    track:fullNeeds evidence: brief, research, plan, execute, review
    on Oct 7, 2026
  2. ABilenduke commented on Oct 7, 2026

    @ABilenduke
    ContributorAuthor

    Time: issue-1

    Generated by worklog render from time.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

  3. ABilenduke commented on Oct 7, 2026

    @ABilenduke
    ContributorAuthor

    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), plus d0573a0 for the parent docs and the sibling spikes at their SHAs · 2026-10-07

    Summary

    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.yaml fragment, 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.yaml holds 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.watch and up --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 ships compose.platform.yaml for 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 sh scripts, 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 .env and the CA and certificate, trust the CA, then print the hosts lines, with an opt-in make hosts that elevates once.
    Secrets: a POSIX script writes a gitignored, mode-600 .env and fills only missing keys.
    Authorize.Net credentials are optional and empty by default; without them payments are disabled. Seed is run --rm api seed (the backend's own command); reset is down -v after 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 %2F preserved. Only nginx did it as non-root with no capability exceptions. Caddy needs NET_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 to rsl-commerce.test plus one leaf with 8 explicit SANs. A foreign name signed by the same root was rejected. Local HSTS is max-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 with default_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-lgtm 0.35.0, which its README says is for development: one OTLP endpoint, Grafana at observe., 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 through make 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.yaml with name:tag@sha256; Dependabot docker-compose bumps 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 from docs/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.
    • up and reset: up always uses --wait, and reset uses --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 named compose*.yaml in the configured directories (FC16-FC17).
    • The resolution: app fragments don't set image:. The platform's wiring files compose/compose.<app>.yaml set it as a literal name:tag@sha256. They merge with the fragment through include: path: [fragment, wiring], which is the documented merge (FA8). Dependabot then watches directories: ["/", "/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_TOKEN can 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
    bootstrap Idempotent setup
    up [PROFILES=…] [LOCAL=backend,…] Start; LOCAL builds the named apps from local checkouts
    down Stop; keeps data
    reset Delete volumes after confirmation; keeps .env and the certificates
    seed docker compose run --rm backend-api seed
    logs [s=…] Follow logs
    status Health, certificate, hosts lines, and whether payments are enabled
    doctor Read-only checks: XDG_RUNTIME_DIR, Docker Desktop disk health, ports 80 and 443
    hosts, trust The two steps that need elevation

    CA, merging RQ-2 and RQ-3: ca.sh uses openssl to create a root with nameConstraints=critical,permitted;DNS:rsl-commerce.test, pathlen:0 and 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: sudo copy plus update-ca-certificates, and certutil into both ~/.pki/nssdb and ~/.local/share/pki/nssdb.

    Hosts: printed by default. make hosts writes a marked # BEGIN rsl-commerce / # END block, idempotently and with one elevation. On WSL2 that's a single UAC prompt through Start-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 make if it's missing.
    • macOS: the sudo password twice; brew install nss for Firefox.
    • Linux: sudo apt install make libnss3-tools, and the sudo password twice.

    Secrets:

    • gen-env.sh is POSIX sh using /dev/urandom, runs under umask 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 server block per host and a default server using ssl_reject_handshake on.
    • A map $http_upgrade WebSocket route on api., with a 1-hour read timeout.
    • Upstream names come from the image's /etc/nginx/templates envsubst.
    • The proxy sets TLS, HSTS (max-age=86400; includeSubDomains) and X-Forwarded-*. Apps set CSP, COOP, CORP, nosniff and 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 internal and 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 /tmp and /var/run/postgresql; health check pg_isready
    Valkey valkey/valkey:9.1 (9.1.2) BSD-3; user: 999:999; /data; health check valkey-cli ping; the backend uses a neutral CACHE_URL=redis://valkey:6379/0 and avoids Redis-8-only commands
    RabbitMQ rabbitmq:4.3-management (4.3.6) user: 999:999; default_queue_type = quorum; the management UI only on 127.0.0.1:15672, not a 9th host
    Mailpit axllent/mailpit:v1.31 plus a 2-line derived image SMTP 1025 internal; UI at mail.
    Meilisearch getmeili/meilisearch:v1.54 (community, MIT) plus a 2-line derived image MEILI_NO_ANALYTICS=true; generated master key
    • Platform-provided values the backend's fragment declares: DATABASE_URL, CACHE_URL, AMQP_URL, SMTP_ADDR, MEILI_URL and its key, and OTEL_EXPORTER_OTLP_ENDPOINT=http://lgtm:4318 with http/protobuf and OTEL_SERVICE_NAME.
    • Backend rules: quorum queues with a DLX and x-delivery-limit are declared explicitly; at-least-once delivery needs overflow=reject-publish; trust X-Forwarded-Proto only from the proxy; apps never send HSTS.

    5. Observability (RQ-5; FB54-FB71)

    • grafana/otel-lgtm:0.35.0, pinned, in the observability profile.
    • It runs as uid 65534 with a read-only root and tmpfs on /tmp, /data and /var/tempo. Grafana is at observe., 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 test profile with the backend set to PAYMENTS_GATEWAY=fake. The fake Authorize.Net handles getHostedPaymentPageRequest, 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 --wait plus a curl --resolve sweep 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>.yaml image lines are the matrix (see §1).
    • Dependabot docker-compose runs 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) and codeql.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 actions plus javascript-typescript;
    • re-pin actions by SHA;
    • Dependabot for github-actions, npm, docker and docker-compose;
    • docker compose config --quiet per 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, and style-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 to api. locally. Its nginx location can 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.md at the matrix's SHAs, through GITHUB_TOKEN, with a gitignored cache for offline builds.
      • The OpenAPI spec comes from the backend's openapi.json release 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 carry frame-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).
      • doctor should 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-action for 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-commerce lines, with design. missing. CurrentUser\Root holds an old mkcert leaf. Bootstrap must reconcile.
    • Experiment side effect. Docker created an empty, root-owned /var/lib/docker/containers directory in the WSL distro during a bind-mount probe. sudo rmdir /var/lib/docker/containers /var/lib/docker removes 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.yaml contracts (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-env dev 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.source label; compose.platform.yaml per the rules; a health endpoint; ADRs at docs/adr/NNNN-slug.md
    backend Idempotent seed and migrate subcommands; a healthcheck subcommand 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.json as a release asset; test identities; OTLP logs
    checkout 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 docs
    commerce 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 ^1 and mermaid 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/containers side effect is confirmed.

    Evidence is in the research:evidence comment. 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

  4. ABilenduke commented on Oct 7, 2026

    @ABilenduke
    ContributorAuthor

    Research evidence: Choose the platform stack

    Evidence base: platform 5db9dab, root d0573a0, backend 38a8548, agent-workflow-tooling c1d006c; 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:

    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;
      • path lists merge;
      • remote OCI and Git sources are supported.

      (include reference; Include how-to)

    • FA9 [sourced] "Services without a profiles attribute are always enabled"; --profile "*" enables every profile. (Profiles)

    • FA10 [sourced] Service options:

      • depends_on takes service_healthy;
      • env_file with required: false is silently ignored when missing;
      • pull_policy: build;
      • extends;
      • images can be pinned @<digest>.

      (Services reference)

    • FA11 [sourced] develop.watch offers sync, rebuild and sync+restart, from Compose 2.22.0. (File watch)

    • FA12 [sourced] compose.override.yaml is loaded automatically, and the docs recommend include for monorepos. (Merge)

    • FA13 [sourced] docker compose publish --resolve-image-digests can't publish bind mounts or services that only build. Git include URLs accept #ref:path. (OCI artifact docs; compose #13362)

    • FA14 [sourced] down -v removes volumes, run --rm removes the one-off container, and up --wait waits 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 and live_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.source label. (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 path list, resolved. The fragment's api got depends_on: postgres: service_healthy, although postgres is defined by the platform.

    • FA21 [experiment] A missing ${BACKEND_SESSION_KEY:?…} failed with "required variable BACKEND_SESSION_KEY is missing a value". An include's env_file doesn't leak into the parent's interpolation.

    • FA22 [experiment] A service defined in both the platform and an include merged silently: the platform's image won, 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.1 loader/include.go:228-275, which calls MergeYaml (Compose v5.6.0 go.mod:10).

    • FA22b [experiment] include: - https://github.com/docker/awesome-compose.git#30f4b7f…:nginx-golang/compose.yaml resolved in 3.0 s without a prompt and was cached under $XDG_CACHE_HOME/docker-compose/.

    • FA23 [experiment] up -d --wait took 14.2 s, and api started only after postgres reported 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 observability added otel; 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.watch rebuilt the image on a source change. It first failed with "cannot take exclusive lock … /run/user/1000", because XDG_RUNTIME_DIR pointed 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 make but has git and curl. This machine's Ubuntu 22.04 has GNU Make 4.3.

    • FA32 [sourced] Installers:

      • just: apt, winget, scoop and an install.sh script (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).
    • FA33 [experiment] Make, just, Task and mise all dry-ran up identically.

      Runner status Binary Friction
      Make 0.11 s — none
      just 0.11 s 6.1 MB Docker {{ }} templates need escaping
      Task 0.35 s 50 MB arguments need a --
      mise 0.48 s 132 MB status failed until wrapped in {% raw %}, and needed mise 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 @ 1c1dc4e per OS:

      • macOS: sudo security add-trusted-cert -d -k /Library/Keychains/System.keychain (truststore_darwin.go:53);
      • Linux: sudo tee plus update-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.

    • FA37 [sourced] Windows trust stores:

      • CertOpenSystemStore reaches "Only current user certificates" (Microsoft Learn);
      • Import-Certificate -CertStoreLocation Cert:\CurrentUser\Root (Import-Certificate);
      • certutil -user targets the user store (certutil).
    • FA38 [sourced] On Windows, -install shows 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.enabled defaults to true, and on Windows Firefox reads CERT_SYSTEM_STORE_CURRENT_USER (EnterpriseRoots.cpp:134-217, mozilla-firefox main cfd140a311);
      • 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, localhostForwarding and dnsTunneling (Windows 11 22H2+) default to true. (WSL filesystems; wsl-config; networking)

    • FA44 [experiment] Read-only probes:

      • powershell.exe, certutil.exe, wslpath and cmd.exe are present; mkcert.exe is 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 -dump and PowerShell X509Certificate2 through its \\wsl.localhost path. It was not present in CurrentUser\Root, and nothing was installed.

    • FA46 [experiment] Existing state:

      • the WSL mkcert CA (SHA-1 88:3B:08…) is in neither Windows store;
      • CurrentUser\Root holds an mkcert leaf (valid 2025-12-13 to 2028-03-13).
    • FA47 [experiment] Hosts files:

      • the Windows hosts ACL is Users read/execute only;
      • the file has 7 rsl-commerce lines and no design.;
      • WSL resolves api. through 10.255.255.254 (DNS tunneling), but not design..
    • FA48 [sourced] DDEV v1.25.4 manages the Windows hosts file from WSL2 with ddev-hostname.exe, which self-elevates with the verb runas (cmd/ddev-hostname/elevate_windows.go:41-80). It can be opted out with WSL2NoWindowsHostsMgt. (ddev)

    • FA49 [sourced] Start-Process -Verb RunAs runs 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\Root needs no admin and no mkcert.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 .gitignore covers only .agent/ (platform/.gitignore:1-2), @ 5db9dab.

    • FA53 [sourced] git-secrets checks staged files against prohibited patterns, and supports --add and --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-ignore matched .env and certs/.
    • 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.

      (Sandbox; Accept Hosted)

    • 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 --rm waited for Postgres to be healthy. down -v removed 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". exposedByDefault is 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 Upgrade and Connection headers 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=+ep and has no USER (caddy-docker 2.11/alpine/Dockerfile:39). The Traefik image has no USER either.

    • 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: ALL fails with "exec caddy failed: Operation not permitted". It works with cap_add NET_BIND_SERVICE, or with a derived image after setcap -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/q1 intact. 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 Protocols with a correct Sec-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_SERVICE
      nginx 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 with pathlen:0 and no name constraints, and an 825-day leaf with 8 SANs.

    • FB26 [experiment] A leaf for rsl-commerce.test plus *.rsl-commerce.test matched the bare domain, api. and design., but not a.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. A www.example.com leaf from the same root failed openssl verify and curl with "permitted subtree violation".

    • FB29 [sourced] Caddy's local CA: a 10-year root and a 7-day intermediate, with skip_install_trust and caddy trust. (Automatic HTTPS)

    • FB30 [experiment] Caddy tls internal produced a root valid to 2036 with pathlen:1 and no name constraints, and 12-hour leaves.

    • FB31 [sourced] step-ca is an online ACME CA. (step-ca)

    • FB32 [sourced] .test is reserved, and registrars "MUST NOT grant" it. (RFC 6761 §6.2)

    • FB33 [sourced] The Chromium HSTS preload list has no test entries. (transport_security_state_static.json)

    • FB34 [sourced] On HSTS errors the browser "MUST terminate the connection" with "no user recourse", and max-age=0 clears the policy. (RFC 6797)

    • FB35 [sourced] .test is 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/docker and 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 chown on the mount point.
    • FB42 [experiment] Behaviour checks:
      • a queue declared without a type came back quorum;
      • Mailpit received an SMTP message;
      • Meilisearch /health reported available, and other routes returned 401 without the key;
      • the PostgreSQL 18.6 data directory was confirmed.
    • 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 chown and then setpriv; 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_DATABASE and a /data volume, 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 /data and 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, /data and /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 local logging driver, and a bind mount of /var/lib/docker/containers came up empty. That probe also left an empty, root-owned /var/lib/docker/containers directory in the WSL distro (confirmed by the lead).
    • FB65 [sourced] Alloy's loki.source.docker reads 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_ENDPOINT and OTEL_EXPORTER_OTLP_PROTOCOL (default http/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 logs is 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() with route.fulfill. (Playwright Network 1.63)

    • FC9 [experiment] Chromium 153 with --host-resolver-rules=MAP *.rsl-commerce.test 127.0.0.1 loaded api. and checkout. with 0 lines in the hosts file. With the self-signed certificate it failed with ERR_CERT_AUTHORITY_INVALID; with ignoreHTTPSErrors it reported isSecureContext: true and stored a __Host- cookie.

    • FC10 [sourced] docker compose config -q validates 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 audit found 15 issues (6 moderate, 9 high).

    • FC16 [sourced] Dependabot:

      • the Docker Compose ecosystem gets version updates but no security updates;
      • cooldown is supported, and directories accepts 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).
    • FC18 [sourced] Cross-repo dispatch needs a PAT or an App with Contents write; GITHUB_TOKEN is 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 api releases)

    • 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/scalar returned 200 and /nope 404.

    • FC30 [experiment] Scalar 1.73.0, as a Vue component inside <ClientOnly>, rendered. Under the VitePress policy its one violation was an eval probe.

    • FC31 [experiment] Scalar defaults to fonts and telemetry on (api-reference-configuration.js:409; base-configuration.js:163), which gave 14 font-src violations to fonts.scalar.com. The eval violation is Zod's Function probe, 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-elem violations 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 audit of 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-mermaid 2.0.17 peers vitepress ^1 and mermaid 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.

      (Structurizr)

    • 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_TOKEN allows 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 in agent-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-compose v5.6.0 config for the include, required-variable and conflict tests;
        • up -d --wait, the observability profile, -f compose.local.yaml and watch;
        • Postgres secret login;
        • gen-env.sh three times, and git secrets --scan;
        • make -n, just --dry-run, task --dry and mise run --dry-run;
        • read-only probes: command -v, $PSVersionTable, wslpath -w, icacls.exe, a Cert:\CurrentUser\Root count, and certutil.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 (once XDG_RUNTIME_DIR was 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.
    • 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 openssl name-constrained root and leaves, checked with openssl verify and curl --cacert --resolve;
        • Caddy capability tests;
        • a Compose project with web, echo, Traefik, Caddy, caddy-internal and nginx, tested with test-proxy.sh and ws-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 info and the log-mount probe.
      • Cleanup: every container, network and volume was removed, the throwaway keys were deleted, and the pulled images remain (about 8 GB).
    • 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.mjs serving three CSP variants, plus nginx-unprivileged running read-only with all capabilities dropped;
        • a Playwright probe recording securitypolicyviolation events, 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.
      • Cleanup: containers and the network were removed.
    • Lead checks (after all runs):
      • the hosts-file checksums and certificate counts are unchanged from the pre-experiment baseline;
      • docker ps -a is empty.

    workflow · claude-code · 2026-10-07 12:13

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stage:doneDonetrack:fullNeeds evidence: brief, research, plan, execute, reviewtype:spikeResearch that answers a question and ends there

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions