Rendered from the repository — the file stays the source of truth.
Infrastructure — project history
Status: Snapshot as of 2026-08-18, written by session C4. Covers
infrastructure/and.github/workflows/through PR #19. Later sessions append below rather than rewriting. Sources: PR descriptions,handovers/,product/feasibility.md,product/security.md.
What was built
- 2026-08-17 — Terraform skeleton (PR #1).
Scaleway provider ~> 2.81: website buckets for docs and marketing,
registry + container namespaces, the
scaleway_containerresource commented out until a first image existed. Local state; the S3 backend block prepared but commented.terraform validatepassing from day one. - 2026-08-17 — First real deployment, spike S-1 / session B1
(PR #11). The
scaleway_containerwent live (marginalia-webapp, fr-par, min-scale 0, 1 vCPU / 2 GB), resource prefix renamed tomarginalia(owner-confirmed product name) before the first apply, plus a manual image-push script. - 2026-08-18 — CI and deploy pipelines, session B2
(PR #13). Three
GitHub Actions workflows:
ci.ymlon every PR (lint, typecheck,bun test, build all three apps,terraform fmt -check+validatewithout credentials),deploy-webapp.yml(docker build → push to the Scaleway registry → update the container via the Containers API → poll → smoke test), anddeploy-static-sites.yml(Astro builds →aws s3 syncto the website buckets). Terraform state moved into a versionedmarginalia-tfstatebucket. All three targets were deployed with owner approval on 2026-08-18: the webapp answers with the auth proxy’s 307, the docs and marketing buckets serve HTTP 200. - 2026-08-18 — Deploy fix (PR #19).
First real Actions dispatch of
deploy-webappexposed a redeploy-call bug (see problems below); the workflow now skips the explicit/redeploywhen the image PATCH already started a rollout.
Decisions and why
-
Scaleway serverless confirmed — spike S-1 decided D-7 (PR #11,
product/feasibility.mdD-7). The single highest risk in the feasibility study was whether SSE streaming survives the Serverless Containers gateway — the docs cover HTTP/2, WebSockets and gRPC but never SSE. B1 ran the spike first, on purpose, against a real deployment. The measured evidence:- a server-paced 500 ms SSE ticker arrived with 499–508 ms client-side deltas — no gateway buffering;
- an 87-event AI-SDK LLM stream arrived token-by-token over ~0.77 s with a
clean
finish/[DONE]— no truncation; - warm TTFB 87–96 ms; cold start from min-scale 0 after 25 min idle: 3.89 s TTFB (407 MB image);
- request-body probes at 512 KB / 1 / 5 / 10 / 20 MB all passed — the user-reported ~1 MB gateway limit did not materialize.
Verdict: serverless stays; the VM fallback text in D-7 is retained for reference only. (D-5’s storage-direct uploads also stay — for RLS and resumability, no longer because of a body limit.)
-
State in a versioned bucket, no locking (session B2, PR #13). Scaleway’s S3-compatible backend has no DynamoDB-equivalent lock table; the accepted trade-off for a single-operator prototype is bucket versioning as the safety net, documented in
infrastructure/AGENTS.md. -
Min-scale 0 as the cost posture. The container idles at €0 (max-scale 2 caps the worst case); demo windows switch to min-scale 1 later (B3). Every infrastructure PR carries an explicit cost-delta section — a convention worth keeping.
-
CI never sees real Supabase values (session B2). The webapp build in CI uses obviously-fake placeholder
NEXT_PUBLIC_SUPABASE_*values; the corresponding repo secrets are placeholders too, because no hosted Supabase project exists yet (SEC-6 governs the real secrets flow —gh secret set, names only in PRs).
Problems and how they were dealt with
-
Deprecated provider arguments on
scaleway_container. When B1 uncommented the container resource, the scaffold-era arguments had been deprecated in provider 2.x; they were replaced with the current ones (image,memory_limit_bytes,https_connections_only,public_endpoint) before the first apply (handovers/2026-08-17-session-b1-spike-streaming.md). -
Generative APIs 403s on the bare
/v1URL. The IAM key’s default project wasn’t the target project, and Scaleway’s Generative APIs then require the project-scoped base URL (https://api.scaleway.ai/<project-id>/v1). Found in B1 when the LLM stream failed; fixed by passing the scoped URL as a container env var via Terraform (commitbcbc258, PR #11). -
A narrowly-scoped IAM key can’t read the account API. Terraform data sources like
scaleway_account_project403 under a key scoped to storage/registry/containers/GenerativeApis; the project ID is passed in asTF_VAR_scw_project_idinstead (B1 handover). -
Terraform state stranded in a worktree. B1’s first apply left state local to the session worktree — flagged in the PR as a hand-off hazard (worktrees get deleted), parked in the main checkout, then properly resolved by B2’s migration. The bootstrap had a chicken-and-egg step: the tfstate bucket can’t live in the state it stores at creation time, so it was created by a targeted apply against the still-local state, then
terraform init -migrate-statemoved the state into it. The obsolete local state file in the main checkout was flagged for the foreman to delete (PR #13). -
Containers API drift — in two rounds. B2 found the current API is
containers/v1(not v1beta1), the field isimage(registry_imageis gone, matching the provider deprecations B1 hit), and observed that a PATCH does not roll out on its own, so the workflow added an explicit POST/redeploy(commitsdd7fa6a,6a9eb96). The first real Actions dispatch then failed at exactly that call: an image-changing PATCH does start a rollout, and/redeployduring it returns 4xx — the deploy had succeeded, only the job died before its smoke test. PR #19 made the explicit redeploy conditional on the PATCH having been a no-op. -
bun testexits 99 under PGlite despite 0 failures. Bun 1.3.14 + pglite 0.5.5 exit with code 99 whenever a test instantiates PGlite, even though all tests pass (Node exits 0). Local runs never noticed — nobody checks$?by hand; CI’s exit-code check surfaced it immediately. CI now accepts exactly theexit 99 && "0 fail"signature and fails everything else (commite425a1e); flagged as worth an upstream report (handovers/2026-08-18-session-b2-ci-deploy.md). -
PGlite cold init can blow Bun’s 5 s hook timeout on CI runners. The first
new PGlite()(WASM compile) intermittently took >5 s on a GitHub runner, failing abeforeAllas(fail) (unnamed). CI runsbun test --timeout 30000(commit89b4fad).Resolution (2026-08-18, session A7, D-9). Both PGlite issues above are gone at the root: DB-backed tests moved to a real Postgres + pgvector (local Supabase stack / CI service container), PGlite was removed, and CI’s exit-99 allowlist and raised timeout were reverted to a plain
bun test. -
workflow_dispatchworkflows aren’t dispatchable until merged. GitHub only indexes them from the default branch, so B2’s one-time deploy ran the identical steps locally; the first post-merge dispatch (which then found the PR #19 bug) was a foreman follow-up. -
memory_limit_bytesdrift. Config says 2147483648, the API stored 2147000000, so unrestricted plans show a benign in-place update. Consciously left unfixed in B2 (the apply was kept strictly targeted); aligning the config value is a noted option. -
The spike route was publicly reachable (SEC-4). B1’s deployed image exposed the unauthenticated
/api/spike-streamon a public URL — token spend if the URL leaked. Accepted for days, not weeks, by the B1 PR; B2’s deploy of current main put it behind A2’s auth proxy (curl-verified 307), and full closure comes when A4 deletes the route (PRs #13, #17).
Where infrastructure stands
Everything deployable is deployed from pipelines: webapp container plus both static-site buckets, with CI gating every PR and state versioned in a bucket. Open per the roadmap: B3 (hosted Supabase, demo-mode scaling, Edge Services / custom domains), per-user rate limiting before public exposure (SEC-7), and security headers (SEC-9).