|
2 | 2 | title: "Build order: the humain-vc unlock flow, step by step" |
3 | 3 | lede: "The execution sequence for Flow 1 (Michael + Aniel, side-by-side thesis corpus building on a hosted augment-it) — each step names its repo, files, and verification so any fresh session can pick up mid-sequence. The strategy and scope cuts live in the ai-labs plan; this is the how." |
4 | 4 | date_created: 2026-07-06 |
5 | | -date_modified: 2026-07-08 |
| 5 | +date_modified: 2026-07-09 |
6 | 6 | authors: |
7 | 7 | - Michael Staton |
8 | 8 | augmented_with: |
9 | 9 | - Claude Code on Claude Fable 5 |
10 | 10 | - Claude Code on Claude Sonnet 5 |
11 | | -semantic_version: 0.0.3.0 |
| 11 | +semantic_version: 0.0.4.0 |
12 | 12 | status: Ready |
13 | 13 | tags: |
14 | 14 | - Plan |
@@ -70,10 +70,20 @@ tags: |
70 | 70 | (flipped from the `optional` dev default for live testing this session — |
71 | 71 | flip back in `.env` when done); the prove script's GATE mode is the |
72 | 72 | repeatable check either way. |
73 | | -- The DO droplet (167.172.42.247) is prepped: Coolify removed, 2GB swap, |
74 | | - Docker 28, ports 80/443 free, SSH via the id_rsa_nopass key. |
75 | | -- **NEXT: step 9** — the deploy tail (9–12) is all that's left. Steps |
76 | | - 1–8 are done and verified locally. |
| 73 | +- **The DO droplet plan was abandoned** (167.172.42.247 — still prepped, |
| 74 | + still paid-for, just not used). Steps 9–10 shipped on **Railway** |
| 75 | + instead, plus a real custom domain — see their sections for why and |
| 76 | + the full list of Railway-specific gotchas hit along the way. |
| 77 | +- **Live URLs, deployed:** `https://augment.didi.sh` (shell, single-tenant |
| 78 | + humain-vc), `wss://ws.augment.didi.sh/ws` (workspace-service). Both |
| 79 | + verified live via headless-browser checks against the real deployed |
| 80 | + URLs, not just localhost. |
| 81 | +- **NEXT: step 12** — the dress rehearsal. Aniel's membership still isn't |
| 82 | + seeded on prod (carried over from Step 2 — his address was never |
| 83 | + confirmed), and nobody has completed an actual human sign-in against |
| 84 | + `augment.didi.sh` yet (dev-token echo is off in prod by design, so this |
| 85 | + genuinely needs a person clicking a real email). Step 11 (corpus sync) |
| 86 | + needs a Railway-native redesign before it's real, but doesn't block 12. |
77 | 87 |
|
78 | 88 | Steps 1–8 are local, each verifiable on the laptop; 9–12 are the deploy |
79 | 89 | tail. Steps marked ⚑ need an operator decision or action first. |
@@ -327,60 +337,129 @@ deferred to its own initiative, not bundled into this flow. |
327 | 337 | `svelte-check` clean on `apps/chat` and `shell`; `tsc --noEmit` clean on |
328 | 338 | `services/workspace`. |
329 | 339 |
|
330 | | -## Step 9 — Deploy augment-it, single-tenant on DigitalOcean |
331 | | - |
332 | | -**Decided 2026-07-06: the repurposed DigitalOcean droplet** |
333 | | -`ubuntu-s-1vcpu-1gb-amd-ams3-01` at **167.172.42.247** (already paid |
334 | | -for; whatever's on it is disposable — code lives on GitHub). Caddy for |
335 | | -TLS at `augment.didi.sh`. |
336 | | - |
337 | | -**Box prepped 2026-07-06:** Coolify (the prior tenant) removed, ports |
338 | | -80/443 freed, 2 GB swapfile active + persisted, Docker 28 present, |
339 | | -~556 MB RAM available, 18 GB disk free. SSH: root@ with the |
340 | | -`id_rsa_nopass` key. |
341 | | - |
342 | | -**The 1 GB constraint:** the full 11-service compose won't fit. Flow 1 |
343 | | -needs only the curator path — run a **flow-minimal compose profile**: |
344 | | -`nats + workspace-service + record-surrealdb-resolver + content-ingest` |
345 | | -(+ Caddy, + the shell as static files). ~400–500 MB resident; add a |
346 | | -**2 GB swapfile** for fetch/compression spikes and build churn (pnpm |
347 | | -install on 1 GB wants swap; bring services up sequentially on first |
348 | | -build). Other microfrontends stay mounted in the shell and error if |
349 | | -poked — consistent with the "no extra work, no isolation" rule. |
350 | | -Escape hatch: DO resize to 2 GB ($12/mo) is two clicks if it strains. |
351 | | - |
352 | | -- Box provisioning; clone; `.env` with: `ACTIVE_CLIENT_ID=humain-vc`, |
353 | | - `DIDI_AUTH=required`, `REQUIRED_ORG_ID=humain.vc`, |
354 | | - `ID_JWKS_URL=https://id.didi.sh/.well-known/jwks.json`, |
355 | | - `ID_ISSUER=https://id.didi.sh`, real API keys (Jina etc.). |
356 | | -- Only humain-vc under `clients/` on the box (isolation by absence). |
357 | | -- Caddy: `augment.didi.sh` → shell static build + `/ws` → workspace :3001; |
358 | | - the shell's `PUBLIC_ID_BASE=https://id.didi.sh` at build. |
359 | | -- **Verify:** `scripts/prove-didi-auth.mjs` with `ID_BASE=https://id.didi.sh |
360 | | - WS_URL=wss://augment.didi.sh/ws` — member in, stranger out. |
361 | | - |
362 | | -## Step 10 ⚑ — DNS + cookie day (Vercel DNS) |
363 | | - |
364 | | -- ⚑ The **pending id records** land first (name `id`: A + AAAA above) — |
365 | | - `fly certs check id.didi.sh` goes green. |
366 | | -- `augment` record → the step-9 box. Both apps now under `.didi.sh`; the |
367 | | - cookie is shared for real (sign in once, both surfaces). |
368 | | -- id-didi-sh CORS config for prod: add `https://augment.didi.sh` to |
369 | | - `cors_origins` (runtime env), redeploy id. |
370 | | -- **Verify:** sign in on augment.didi.sh; didi_session Domain=.didi.sh in |
371 | | - devtools; badge lights on reload. |
372 | | - |
373 | | -## Step 11 — Corpus sync, option A (box ↔ R2 ↔ laptop) |
374 | | - |
375 | | -- rclone remote for the existing R2 account on the box + laptop; bucket |
376 | | - prefix `corpus/humain-vc/`. |
377 | | -- Box: cron/systemd timer `rclone sync /srv/augment-it/clients/humain-vc |
378 | | - r2:…` (push, scheduled + post-session manual); laptop pulls on demand. |
379 | | -- **Single-writer discipline documented in the repo README**: while the |
380 | | - team works hosted, the box is authoritative; Michael's local edits go |
381 | | - through R2 deliberately, never concurrently. |
382 | | -- **Verify:** add a source hosted → appears on laptop after pull; |
383 | | - checksums match. |
| 340 | +## Step 9 — Deploy augment-it, single-tenant ✅ DONE 2026-07-09 (Railway, not DigitalOcean) |
| 341 | + |
| 342 | +**Platform changed after re-checking the DO droplet's live numbers**: the |
| 343 | +`167.172.42.247` box (prepped 2026-07-06) turned out to have only ~112MB |
| 344 | +free / 537MB available RAM before running a single one of our own |
| 345 | +services — the "escape hatch: resize to 2GB if it strains" language in |
| 346 | +the original plan undersold how tight it already was. A leftover |
| 347 | +`coolify-proxy` container was also still holding ports 80/443, meaning |
| 348 | +"ports freed" was stale too. Given very few users and no prior DO ops |
| 349 | +investment (vs. real Fly.io experience from id-didi-sh this same week), |
| 350 | +the operator chose **Railway** over both DO and Fly — Railway's |
| 351 | +multi-service-project model is the closest 1:1 match to this repo's |
| 352 | +actual docker-compose shape. The DO droplet is abandoned for this flow |
| 353 | +(still paid-for and untouched otherwise). |
| 354 | + |
| 355 | +**8 Railway services**, one project (`augment-it`, workspace "The |
| 356 | +Lossless Group"): `nats` (official image, custom start command — see |
| 357 | +gotcha below), `workspace-service`, `record-surrealdb-resolver`, |
| 358 | +`content-ingest`, `prompt-runner` (all four Dockerfile builds, |
| 359 | +`rootDirectory` = `/services/<name>`), and three frontends — |
| 360 | +`shell`, `strategy-curator`, `chat` — each **also** Dockerfile-built |
| 361 | +(not Railway's Railpack auto-builder; see gotcha below), full-repo |
| 362 | +build context (they need the pnpm workspace), each producing its own |
| 363 | +static `dist/` served via `serve`. |
| 364 | + |
| 365 | +**Two Railway Volumes**, deliberately NOT shared (confirmed via Railway |
| 366 | +docs + support: a volume is strictly single-service): `content-ingest` |
| 367 | +owns `/clients` (the real corpus filesystem, read-write); `workspace-service` |
| 368 | +gets its own tiny `/data` volume, self-seeded on every boot via its |
| 369 | +`deploy.startCommand` (`mkdir -p /data/clients/humain-vc && echo |
| 370 | +DEFAULT_DOMAIN_TYPE=thesis > .../.env && npm start`) rather than |
| 371 | +uploading the real `clients/humain-vc/.env` (which also holds unrelated |
| 372 | +Decile Hub credentials workspace-service has no business touching). |
| 373 | + |
| 374 | +**Real gotchas hit and fixed, worth knowing before touching this again:** |
| 375 | +- Railway's CLI (`environment edit --service-config`, dot-path form) |
| 376 | + silently no-ops in the version used this session — `{"committed":false, |
| 377 | + "message":"No changes to apply"}` regardless of value. The **JSON patch |
| 378 | + form** (`environment edit --json`) works reliably; used for everything. |
| 379 | +- `railway volume add` panics (Rust `unwrap()` on `None`) in this CLI |
| 380 | + version — created volumes via direct GraphQL (`volumeCreate` mutation) |
| 381 | + instead. |
| 382 | +- `nats-server` does **not** accept `-max_payload` as a CLI flag (matches |
| 383 | + this repo's own `nats.conf` comment from months ago) — needed an inline |
| 384 | + generated config file. First attempt (`printf '...\n...\n'`) corrupted |
| 385 | + across the Railway CLI → GraphQL → container `sh -c` chain; the |
| 386 | + **multi-`echo`, no-embedded-newlines** form is what actually survives: |
| 387 | + `echo port: 4222 > /tmp/nats.conf && echo http_port: 8222 >> ... && |
| 388 | + nats-server -c /tmp/nats.conf`. |
| 389 | +- Railway's **Railpack builder auto-detected this repo as a turborepo** |
| 390 | + (`turbo.json` exists at root) and unconditionally ran the root |
| 391 | + package.json's `build` script (`turbo run build`) for the three |
| 392 | + frontends, ignoring any custom `buildCommand` override — and `turbo` |
| 393 | + was never actually an installed binary here. Fixed by giving `shell`, |
| 394 | + `strategy-curator`, and `chat` their own Dockerfiles (direct `pnpm |
| 395 | + --filter <pkg> build`), same pattern as the four backend services. |
| 396 | +- A Docker `ARG` that's declared but never passed resolves to an **empty |
| 397 | + string**, not `undefined` — `?? default` doesn't catch it. Two real |
| 398 | + bugs from this: (1) `shell/rsbuild.config.ts`'s remote-URL fallbacks |
| 399 | + had to change from `??` to `||`; (2) module-federation `assetPrefix` |
| 400 | + was only set for `dev`, not `output` (the field that also covers |
| 401 | + production builds) — missing it meant `chat`'s and `strategy-curator`'s |
| 402 | + async sub-chunks resolved as relative paths against the **shell's** |
| 403 | + origin instead of their own, 404ing into the shell's SPA-fallback HTML |
| 404 | + ("SyntaxError: Unexpected token '<'"). Only reproduces cross-origin — |
| 405 | + local federation dev never surfaces it. Both fixed with |
| 406 | + `output.assetPrefix` env-configured per remote. |
| 407 | +- Railway auto-injects its own `PORT` (8080) for any service — this |
| 408 | + silently mismatched the public domain's configured target port (3001) |
| 409 | + for `workspace-service` until `PORT=3001` was set explicitly as a |
| 410 | + service variable. |
| 411 | + |
| 412 | +**Verify, as done:** headless-Chromium checks against the live deployed |
| 413 | +URLs (not just localhost) — anonymous visitor → full wall, zero |
| 414 | +unexpected console errors (only the 12 out-of-scope remotes' harmless |
| 415 | +`localhost` failures, unchanged from local); `workspace-service`'s |
| 416 | +`/config` endpoint live; `scripts/prove-didi-auth.mjs`'s `GATE=1` mode |
| 417 | +run against the real deployed `wss://ws.augment.didi.sh/ws` — anonymous |
| 418 | +correctly rejected 4401. Member-admitted couldn't be scripted against |
| 419 | +prod `id.didi.sh` (dev-token echo is deliberately disabled there) — that |
| 420 | +leg needs a real human sign-in (Step 12). |
| 421 | + |
| 422 | +## Step 10 — DNS + cookie day ✅ DONE 2026-07-09 (Railway custom domains, not DO/Caddy) |
| 423 | + |
| 424 | +Two Railway custom domains, both required under `*.didi.sh` for the |
| 425 | +`didi_session` cookie (`Domain=.didi.sh`) to actually reach them — the |
| 426 | +shell's own page origin AND workspace-service's WS endpoint (every |
| 427 | +federated remote connects to workspace-service directly, so it's the one |
| 428 | +that must share the cookie domain; the remotes' own static-asset |
| 429 | +origins don't need to): |
| 430 | + |
| 431 | +- `augment.didi.sh` → `shell` (CNAME + TXT ownership-verification record, |
| 432 | + added at Vercel DNS by the operator; validated + cert issued within |
| 433 | + minutes). |
| 434 | +- `ws.augment.didi.sh` → `workspace-service` (same pattern). |
| 435 | +- `id-didi-sh`'s prod `cors_origins` — **was empty** (`config/runtime.exs` |
| 436 | + never set it; only `dev.exs` had `localhost:3100`), meaning every |
| 437 | + cross-origin browser call to `id.didi.sh` in production had been |
| 438 | + silently CORS-rejected since it went live, just never surfaced because |
| 439 | + augment-it wasn't deployed yet. Added `["https://augment.didi.sh"]`, |
| 440 | + deployed to Fly. |
| 441 | +- `PUBLIC_WS_URL` rebuilt into `shell`/`strategy-curator`/`chat` as |
| 442 | + `wss://ws.augment.didi.sh/ws`. |
| 443 | +- **Verify:** `curl -H "Origin: https://augment.didi.sh" -X OPTIONS |
| 444 | + https://id.didi.sh/api/magic-links` returns |
| 445 | + `access-control-allow-origin: https://augment.didi.sh` + |
| 446 | + `access-control-allow-credentials: true`. Live browser check against |
| 447 | + `https://augment.didi.sh` clean (see Step 9). Real sign-in + cookie |
| 448 | + attachment still needs a human (Step 12) — dev-token echo being off in |
| 449 | + prod is a feature, not a gap to route around. |
| 450 | + |
| 451 | +## Step 11 — Corpus sync ⚑ NEEDS REVISITING (Railway volumes replace the DO-box assumption) |
| 452 | + |
| 453 | +Written when Step 9 targeted a DO droplet with a filesystem an rclone |
| 454 | +timer could reach directly. On Railway, `content-ingest`'s corpus lives |
| 455 | +on a **Railway Volume** — reachable via `railway volume files` (CLI, hit |
| 456 | +its own bugs this session — see Step 9) or `railway ssh`, not a plain |
| 457 | +box path a cron job can rclone from directly. The single-writer |
| 458 | +discipline (team writes hosted; Michael's local edits sync deliberately, |
| 459 | +never concurrently) still holds as a policy — the *mechanism* needs a |
| 460 | +Railway-native answer (a periodic job inside `content-ingest` itself |
| 461 | +pushing to R2, most likely) before this step is actually done. Not |
| 462 | +blocking Step 12 — humain-vc's corpus is empty-ish today either way. |
384 | 463 |
|
385 | 464 | ## Step 12 — Dress rehearsal (the acceptance run) |
386 | 465 |
|
|
0 commit comments