# SUBSTRATE.md — windy-git What runs where, what is truth, and what is only a cache. ## Hosts | Rung | Host | Role | Status | |---|---|---|---| | **R0** | **Veron 1** (`Veron-1-5090`, WireGuard `10.10.0.6`) | everything | **current** | | R1 | dedicated VPS (**not Kit 0**) | control plane | on first external push | | R2 | VPS + read replica, dedicated runner box | split | p95 clone > 3s | **Veron 1, measured 2026-08-11:** 24 cores · 251 GB RAM · 3.6 TB root, 978 GB free · load 1.52 · $0/mo. ⛔ **Kit 0 (72.60.118.54) is never a host for this service.** D-4, and there is a boot guard in `api/app/main.py` that refuses to start there in production. ## Ports (all bound to localhost; the tunnel is the only ingress) | Port | Service | |---|---| | 8600 | `windy-git-api` — our plane | | **3080** | Gitea — host 3000 and 3300 are taken by resident projects on Veron 1 | | 5432 | Postgres | | 2000 | cloudflared metrics (probe target) | ## Ingress — Cloudflare Tunnel `windy-git` Zone `windygit.com` = `9d8637dcac3415607b2116e6099fe567` · account `193b347aedeaafe35de0b5a534b2d9aa` · **Free plan**. | Hostname | → | |---|---| | `app.windygit.com` | Gitea :3080 — UI **and** git over HTTPS | | `api.windygit.com` | our plane :8600 | | `models.windygit.com` | HF-compatible endpoint :8600 (v2) | | `windygit.com` | Cloudflare Pages — marketing, Grant-gated | **No inbound port is opened.** The tunnel connects outbound, so the residential dynamic IP is irrelevant. ⚠️ **All hostnames are single-level subdomains, on purpose.** Free Universal SSL covers `windygit.com` + `*.windygit.com` and stops there. A two-level name needs Advanced Certificate Manager ($10/mo) and without it the request dies in the TLS handshake — `curl` exit 35, **no HTTP status at all** — *before* the app is ever consulted, so a perfect service presents as "the app is broken." ## Storage — what is truth | Store | Holds | Truth? | |---|---|---| | `/srv/windygit/git` (local NVMe) | **git object databases** | **truth** | | Postgres `windgit` | repos, grants, versions, tokens, mirrors | **truth** | | Gitea's own DB | Gitea's private state | component-owned; we never write it (I-1) | | R2 `windy-git-lfs` | LFS objects | truth for blobs | | R2 `windy-git-artifacts` | CI artifacts, logs | derived | | R2 `windy-git-backups` | nightly pg_dump + git bundles | derived | | GitHub mirror | full copy of every repo | **belt and suspenders (I-4)** | | 3 TB HDD | periodic cold copy | derived | **I-3: git objects never go to object storage; LFS blobs never go to host disk.** ## Pinned versions | Component | Version | Note | |---|---|---| | Gitea | `1.24.6` | **exact pin, never `latest`** (G2.1). A drift test fails `make check` if the running version differs. | | Postgres | `16-alpine` | | | Python | 3.12 | matches every sibling cell | ## Credentials All in the fleet lockbox, injected by env, **never committed**. `make check` fails on any `cfat_` / `cfut_` / `gh[pousr]_` / `et_plt_` literal in the tree. ### ⚠️ NAMED DEBT — the R2 credential is account-wide **As of 2026-08-11 this cell holds the Cloudflare god token as its R2 credential.** R2's S3 credentials are derived from an API token (access key id = the token's id, secret = SHA-256 of its value), and **no token available to this session has permission to mint a new one** — creating tokens is dashboard-only or needs a token-creating token. So the wiring was proven with the god token rather than blocked on it. This is recorded, not hidden, because an account-wide token is an acceptable named debt and an unacceptable invisible one. **GATE: this must be replaced with a scoped R2 token BEFORE strand G7 lands CI runners on this host.** I-5 says runners execute untrusted code and must not share a kernel with credentials scoped beyond their own job; a god token with R2 + Workers + Pages + WAF + SSL rights sitting on the same box as a runner is exactly the thing I-5 exists to prevent. Minting one is a two-minute job in the Cloudflare dashboard: **R2 → Manage R2 API Tokens → Create → Object Read & Write, scoped to the three `windy-git-*` buckets.** Then set `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` in `/srv/windygit/src/.env` and redeploy. ⚠️ The Cloudflare **god token has Zone:Read but no DNS:Edit.** Use the DNS:Edit token for record creation. ## Backups (G0.9) Nightly `pg_dump` → R2 · nightly `git bundle` per repo → R2 · **quarterly restore drill via `make restore-drill`, with a written, dated result.** The ecosystem currently has no rehearsed restore for anything, anywhere.