714c2aed86
The deploy docs predated the GCP name-alignment and described infrastructure that no longer exists. Discovered during the v0.31.4 deploy. Corrections: - Project wiggleverse-rfc -> wiggleverse-ohm; VM rfc-app -> ohm-rfc-app; install path /opt/rfc-app -> /opt/ohm-rfc-app; system user + service rfc-app -> ohm-rfc-app; external IP 34.132.29.41 -> 136.116.40.66. - SSH is now IAP-only (direct port 22 times out): document --tunnel-through-iap on every gcloud compute ssh. - Meta repo wiggleverse/meta -> wiggleverse/ohm-content. - Two-remote reality: the VM's git origin is git.benstull.org/benstull/ rfc-app, a SEPARATE Gitea from the release one (git.wiggleverse.org) that does not auto-mirror — so a release must be pushed to the benstull remote before the VM can fetch it. The VM tracks a detached release TAG, not a branch. - Frontend-only changes are live once dist/ is rebuilt (nginx serves it directly); pip install only when requirements.txt changed. Docs-only; no version bump (cf. the docs-only commit after v0.31.3). The First-Time Deployment blocks keep the structural commands with a substitution note rather than unvalidated rewrites. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
385 lines
16 KiB
Markdown
385 lines
16 KiB
Markdown
# RFC App — Deployment Reference & New-Session Prompt
|
|
|
|
Use this document as:
|
|
1. A reference for the current `ohm.wiggleverse.org` deployment
|
|
2. A prompt to paste into a new Claude session to deploy a new version
|
|
|
|
---
|
|
|
|
## What This App Is
|
|
|
|
The **RFC App** is a single-process FastAPI + SQLite + React + Vite + Tiptap web application that hosts the Wiggleverse RFC framework — a platform for proposing, discussing, editing, and graduating formal RFCs (Requests for Comments) that define vocabulary for digital representations of humans. It is the primary interface for the Open Human Model (OHM) working group.
|
|
|
|
The v1 build is complete (8 slices shipped, 125 passing integration tests). New sessions extend it by picking from the §19.2 backlog.
|
|
|
|
---
|
|
|
|
## Infrastructure Overview
|
|
|
|
### Host
|
|
|
|
The RFC app runs on its own dedicated GCP VM in a separate project from the Gitea VM. The two coexist under `wiggleverse.org` but are otherwise unrelated infrastructure.
|
|
|
|
> **⚠️ Infrastructure was realigned (GCP name-alignment, ~2026-05).** The
|
|
> GCP project, VM, install path, system user, and systemd unit were all
|
|
> renamed, the static IP changed, and SSH is now **IAP-only** (direct
|
|
> port-22 connections time out). The tables below are the current truth;
|
|
> if you find an older clone of this doc naming `wiggleverse-rfc` /
|
|
> `rfc-app` / `/opt/rfc-app` / `34.132.29.41`, it predates the alignment.
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| GCP project | `wiggleverse-ohm` |
|
|
| VM name | `ohm-rfc-app` |
|
|
| VM type | e2-small |
|
|
| Zone | us-central1-a |
|
|
| OS | Debian 12 (bookworm) |
|
|
| External IP | `136.116.40.66` |
|
|
| SSH | **IAP-only** — `gcloud compute ssh … --tunnel-through-iap` (port 22 is firewalled off the public internet) |
|
|
| OS Login SSH user | `ben_wiggleverse_org` (auto-derived; you don't type it) |
|
|
| App system user | `ohm-rfc-app` |
|
|
|
|
For reference, the separate Gitea VM is `wiggleverse` project / `gitea` VM / 34.55.46.221.
|
|
|
|
### DNS
|
|
|
|
| Record | Type | Value | Proxy |
|
|
|--------|------|-------|-------|
|
|
| `ohm.wiggleverse.org` | A | `136.116.40.66` | DNS-only (gray cloud) |
|
|
| `_dmarc.wiggleverse.org` | TXT | `v=DMARC1; p=none; rua=mailto:ben@wiggleverse.org` | n/a |
|
|
|
|
> Note: `ohm.wiggleverse.org` uses **Let's Encrypt via certbot** directly on the VM. Keep the A record **DNS only (gray cloud)** — Cloudflare Flexible SSL would conflict with certbot.
|
|
|
|
SPF (`v=spf1 include:_spf.google.com ~all`) and DKIM (`google._domainkey`) for `wiggleverse.org` are already in place via Workspace.
|
|
|
|
### Software Stack
|
|
|
|
| Component | Details |
|
|
|-----------|---------|
|
|
| Backend | Python 3.11, FastAPI, uvicorn (single process) |
|
|
| Database | SQLite in WAL mode at `/opt/ohm-rfc-app/backend/data/rfc-app.db` |
|
|
| Frontend | React 19, Vite 8, Tiptap 3, React Router 7 |
|
|
| Web server | nginx — serves `frontend/dist/` as static SPA, proxies `/api/` and `/auth/` to uvicorn on `127.0.0.1:8000` |
|
|
| Process manager | systemd unit `ohm-rfc-app.service`, runs as `ohm-rfc-app` system user |
|
|
| TLS | Let's Encrypt via certbot |
|
|
| Content Gitea (the bot's writes) | Gitea at `git.wiggleverse.org`, org `wiggleverse`, meta repo `ohm-content` (`wiggleverse/ohm-content`), bot service account `rfc-bot` |
|
|
| Code-deploy source (the VM's git origin) | **`https://git.benstull.org/benstull/rfc-app.git`** — a *different* Gitea from the content one. See the two-remote note under "Deploying a New Version." |
|
|
| Email | Google Workspace SMTP relay (`smtp-relay.gmail.com:587`), AUTH'd as `ben@wiggleverse.org`, From `notifications@wiggleverse.org` |
|
|
|
|
### Key Paths on the VM
|
|
|
|
| Path | Contents |
|
|
|------|---------|
|
|
| `/opt/ohm-rfc-app/` | App root (owned by `ohm-rfc-app` user) |
|
|
| `/opt/ohm-rfc-app/backend/.env` | All secrets and config (mode 0600) |
|
|
| `/opt/ohm-rfc-app/backend/data/rfc-app.db` | SQLite database |
|
|
| `/opt/ohm-rfc-app/frontend/dist/` | Built React SPA (served by nginx) |
|
|
| `/etc/nginx/sites-available/ohm.wiggleverse.org` | nginx vhost config |
|
|
| `/etc/systemd/system/ohm-rfc-app.service` | systemd unit |
|
|
|
|
---
|
|
|
|
## Architecture Invariants
|
|
|
|
- **The bot is the only Git writer.** Every commit, branch, and PR flows through `backend/app/bot.py`. No module calls Gitea's write API directly. Every action is audited in the `actions` table with an `On-behalf-of:` commit trailer.
|
|
- **Git is truth; SQLite is the cache.** The `cached_*` tables are written only by the Gitea webhook receiver or the 5-minute background reconciler. User actions trigger Git ops; the cache follows.
|
|
- **Single process, single SQLite file.** Never set `--workers > 1` on uvicorn. If scale is needed, the spec calls for a Postgres migration first.
|
|
|
|
---
|
|
|
|
## Gitea Setup (one-time)
|
|
|
|
These are already done for `ohm.wiggleverse.org`. Document here for replication.
|
|
|
|
### Bot service account
|
|
|
|
Created in Gitea as `rfc-bot`. Token scopes: `write:repository`, `write:user`, `write:admin`. Token stored in `.env` as `GITEA_BOT_TOKEN`.
|
|
|
|
### Org
|
|
|
|
`wiggleverse` org exists in Gitea. `rfc-bot` is an Owner of the org.
|
|
|
|
### Meta repo
|
|
|
|
`wiggleverse/ohm-content` (the `META_REPO` value is `ohm-content`) — seeded by `scripts/seed_meta_repo.py`. Contains `PHILOSOPHY.md`, `README.md`, `CONTRIBUTING.md`, and `rfcs/` directory. Gitea webhook registered to `https://ohm.wiggleverse.org/api/webhooks/gitea`.
|
|
|
|
### OAuth2 app
|
|
|
|
Registered in Gitea Site Administration → Integrations → OAuth2 Applications:
|
|
- Name: `RFC App`
|
|
- Redirect URI: `https://ohm.wiggleverse.org/auth/callback`
|
|
- Client ID and secret stored in `.env`
|
|
|
|
---
|
|
|
|
## Workspace Setup (one-time, for email)
|
|
|
|
Email sends via Google Workspace SMTP relay. Already configured for `wiggleverse.org`; document here for replication.
|
|
|
|
- **Admin console → Apps → Google Workspace → Gmail → Routing → SMTP relay service** with a rule named `RFC App`:
|
|
- Allowed senders: "Only addresses in my domains"
|
|
- Authentication: Require SMTP Authentication ☑ (AUTH path — current setup), OR allowlist 34.132.29.41 (no-AUTH alternative)
|
|
- Encryption: Require TLS encryption ☑
|
|
- **Google Group** `notifications@wiggleverse.org` (Access type: Custom, External posters allowed so reply mail lands; member delivery set to "No email").
|
|
- **App password** generated on `ben@wiggleverse.org`. **Critical:** the Workspace account here is linked to a personal `benstull@gmail.com` Google account; when generating an app password, watch the avatar in the top-right of myaccount.google.com — it silently switches back to the personal account, and consumer-Gmail app passwords are rejected by Workspace's SMTP relay with `535 5.7.8 BadCredentials`. Confirm the avatar shows the Workspace account *every time* before generating.
|
|
- **DMARC** TXT record at `_dmarc.wiggleverse.org` (added; see DNS table above).
|
|
|
|
---
|
|
|
|
## Environment Variables
|
|
|
|
Full `.env` for production (file lives at `/opt/rfc-app/backend/.env`, mode 0600):
|
|
|
|
```ini
|
|
# Gitea
|
|
GITEA_URL=https://git.wiggleverse.org
|
|
GITEA_BOT_USER=rfc-bot
|
|
GITEA_BOT_TOKEN=<bot token from Gitea>
|
|
GITEA_ORG=wiggleverse
|
|
META_REPO=meta
|
|
|
|
# OAuth
|
|
OAUTH_CLIENT_ID=<from Gitea OAuth app>
|
|
OAUTH_CLIENT_SECRET=<from Gitea OAuth app>
|
|
|
|
# App
|
|
APP_URL=https://ohm.wiggleverse.org
|
|
SECRET_KEY=<openssl rand -hex 32>
|
|
DATABASE_PATH=/opt/rfc-app/backend/data/rfc-app.db
|
|
OWNER_GITEA_LOGIN=ben.stull
|
|
GITEA_WEBHOOK_SECRET=<openssl rand -hex 32>
|
|
|
|
# LLM
|
|
ENABLED_MODELS=claude
|
|
ANTHROPIC_API_KEY=<key>
|
|
|
|
# Email — Google Workspace SMTP relay (§15.4)
|
|
# Strip the spaces Google shows in the 16-char app password (or quote
|
|
# the value) — sourced as shell, spaces split the value.
|
|
# Alternative: leave SMTP_USER/SMTP_PASSWORD empty and switch the relay
|
|
# rule to IP-based; the app skips SMTP AUTH when SMTP_USER is empty.
|
|
SMTP_HOST=smtp-relay.gmail.com
|
|
SMTP_PORT=587
|
|
SMTP_USER=ben@wiggleverse.org
|
|
SMTP_PASSWORD=<app password, no spaces>
|
|
SMTP_STARTTLS=1
|
|
EMAIL_FROM=notifications@wiggleverse.org
|
|
EMAIL_FROM_NAME=Wiggleverse
|
|
EMAIL_ENABLED=1
|
|
EMAIL_BUNDLE_THRESHOLD=5
|
|
WEBHOOK_EMAIL_BOUNCE_SECRET=
|
|
|
|
# Hygiene scheduler
|
|
HYGIENE_TICK_SECONDS=3600
|
|
```
|
|
|
|
---
|
|
|
|
## Deploying a New Version
|
|
|
|
> **⚠️ Two-remote step — do this FIRST.** The framework's release flow
|
|
> (branches, PRs, version tags) happens on **`git.wiggleverse.org`**
|
|
> (`ben.stull/rfc-app`). But the VM's git origin is a *separate* Gitea,
|
|
> **`git.benstull.org/benstull/rfc-app`**, which does **not** auto-mirror
|
|
> from the release Gitea. So after a release is merged + tagged on
|
|
> `git.wiggleverse.org`, you must push `main` and the new tag to the
|
|
> `benstull` remote before the VM can fetch them:
|
|
> ```bash
|
|
> # from your local rfc-app clone, on main at the merged release tip:
|
|
> git push benstull main vX.Y.Z # benstull = git@git.benstull.org:benstull/rfc-app.git
|
|
> ```
|
|
> If `git ls-remote benstull vX.Y.Z` comes back empty, the VM cannot see
|
|
> the release yet — push it first. (Wiring the two Gitea instances to
|
|
> mirror would remove this step; until then it's manual.)
|
|
|
|
SSH into the VM (IAP-only — the `--tunnel-through-iap` flag is required):
|
|
```bash
|
|
gcloud compute ssh ohm-rfc-app --zone=us-central1-a --project=wiggleverse-ohm --tunnel-through-iap
|
|
```
|
|
|
|
Fetch + check out the release tag (the VM tracks a **detached** tag, not
|
|
a branch), then restart. Backend deps only need reinstalling when
|
|
`requirements.txt` changed:
|
|
```bash
|
|
sudo -u ohm-rfc-app git -C /opt/ohm-rfc-app fetch origin --tags
|
|
sudo -u ohm-rfc-app git -C /opt/ohm-rfc-app checkout vX.Y.Z
|
|
# only if backend deps changed:
|
|
sudo -u ohm-rfc-app /opt/ohm-rfc-app/backend/.venv/bin/pip install \
|
|
-r /opt/ohm-rfc-app/backend/requirements.txt
|
|
sudo systemctl restart ohm-rfc-app
|
|
```
|
|
|
|
For frontend changes, build on the VM directly (Node 20+ is already there):
|
|
```bash
|
|
cd /opt/ohm-rfc-app/frontend && sudo -u ohm-rfc-app npm ci
|
|
sudo -u ohm-rfc-app npm run build
|
|
```
|
|
|
|
Smoke test (asset hashes change on every rebuild, so a match proves the
|
|
new bundle is live):
|
|
```bash
|
|
# public:
|
|
curl -s -o /dev/null -w '%{http_code} %{remote_ip}\n' https://ohm.wiggleverse.org/
|
|
curl -s https://ohm.wiggleverse.org/ | grep -oE 'assets/index-[A-Za-z0-9_-]+\.(js|css)'
|
|
# backend startup line, on the VM:
|
|
sudo journalctl -u ohm-rfc-app -n 20 --no-pager | grep 'RFC app started'
|
|
```
|
|
|
|
`npm ci` installs strictly from the committed `package-lock.json` and
|
|
will not regenerate it. Using `npm install` here causes the VM's npm
|
|
to rewrite the lockfile in place (notably stripping `libc` fields on
|
|
optional rollup native packages), which then conflicts with `git
|
|
checkout <tag>` on the next deploy.
|
|
|
|
The output lands in `/opt/ohm-rfc-app/frontend/dist/` owned by `ohm-rfc-app` — nginx serves it directly, no copy step needed.
|
|
|
|
(Building locally and `gcloud compute scp`-ing the dist also works. Plain `rsync -e ssh` from the Mac fails because OS Login uses short-lived SSH certs that only the gcloud wrapper can mint interactively.)
|
|
|
|
Schema migrations run automatically on restart (append-only, safe to re-run).
|
|
|
|
---
|
|
|
|
## First-Time Deployment (new server)
|
|
|
|
> **Note on names:** the command blocks in this section predate the GCP
|
|
> name-alignment and still spell the pre-alignment project/VM/path/user
|
|
> (`wiggleverse-rfc` / `rfc-app` / `/opt/rfc-app` / `rfc-app` /
|
|
> `34.132.29.41`). They're kept as the structural reference. When standing
|
|
> up a box today, substitute the current values from the Host/Paths tables
|
|
> at the top: project `wiggleverse-ohm`, VM `ohm-rfc-app`, install path
|
|
> `/opt/ohm-rfc-app`, system user `ohm-rfc-app`, service `ohm-rfc-app`, IP
|
|
> `136.116.40.66`, and SSH via `--tunnel-through-iap`.
|
|
|
|
### 1. Add DNS record
|
|
Add `ohm.wiggleverse.org` → 34.132.29.41 as an A record in Cloudflare, **DNS only (gray cloud)**. Do not proxy — certbot needs to reach the VM directly.
|
|
|
|
### 2. Host prep
|
|
```bash
|
|
sudo useradd --system --shell /usr/sbin/nologin --home-dir /opt/rfc-app rfc-app
|
|
sudo mkdir -p /opt/rfc-app
|
|
sudo chown rfc-app:rfc-app /opt/rfc-app
|
|
sudo -u rfc-app git clone https://git.wiggleverse.org/ben.stull/rfc-app.git /opt/rfc-app
|
|
```
|
|
|
|
### 3. Python venv
|
|
```bash
|
|
sudo -u rfc-app python3 -m venv /opt/rfc-app/backend/.venv
|
|
sudo -u rfc-app /opt/rfc-app/backend/.venv/bin/pip install \
|
|
-r /opt/rfc-app/backend/requirements.txt
|
|
```
|
|
|
|
### 4. Write .env
|
|
```bash
|
|
sudo -u rfc-app cp /opt/rfc-app/backend/.env.example /opt/rfc-app/backend/.env
|
|
sudoedit /opt/rfc-app/backend/.env
|
|
sudo chmod 600 /opt/rfc-app/backend/.env
|
|
sudo chown rfc-app:rfc-app /opt/rfc-app/backend/.env
|
|
```
|
|
|
|
### 5. Seed meta repo
|
|
```bash
|
|
sudo -u rfc-app -H bash -c \
|
|
'cd /opt/rfc-app/backend && .venv/bin/python ../scripts/seed_meta_repo.py'
|
|
```
|
|
|
|
### 6. Build the frontend (on the VM)
|
|
```bash
|
|
cd /opt/rfc-app/frontend && sudo -u rfc-app npm ci
|
|
sudo -u rfc-app npm run build
|
|
```
|
|
|
|
### 7. nginx
|
|
```bash
|
|
sudo cp /opt/rfc-app/deploy/nginx/ohm.wiggleverse.org.conf \
|
|
/etc/nginx/sites-available/ohm.wiggleverse.org
|
|
sudo ln -s /etc/nginx/sites-available/ohm.wiggleverse.org \
|
|
/etc/nginx/sites-enabled/
|
|
sudo usermod -a -G rfc-app www-data
|
|
sudo chmod -R g+rX /opt/rfc-app/frontend/dist
|
|
sudo nginx -t && sudo systemctl reload nginx
|
|
```
|
|
|
|
### 8. Let's Encrypt
|
|
```bash
|
|
sudo certbot --nginx -d ohm.wiggleverse.org
|
|
```
|
|
|
|
### 9. systemd
|
|
```bash
|
|
sudo cp /opt/rfc-app/deploy/systemd/rfc-app.service /etc/systemd/system/
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now rfc-app
|
|
sudo systemctl status rfc-app
|
|
```
|
|
|
|
### 10. Smoke test
|
|
Visit `https://ohm.wiggleverse.org`:
|
|
1. Landing page renders with sign-in button
|
|
2. Sign in with Gitea OAuth → catalog loads
|
|
3. `+ Propose New RFC` opens the propose modal
|
|
4. `/admin` loads the four-tab home base
|
|
5. `/settings/notifications` renders all five sections
|
|
|
|
---
|
|
|
|
## Day-2 Operations
|
|
|
|
### Logs
|
|
```bash
|
|
sudo journalctl -u rfc-app -f
|
|
sudo journalctl -u rfc-app -p err
|
|
```
|
|
|
|
### Database backup
|
|
```bash
|
|
sqlite3 /opt/rfc-app/backend/data/rfc-app.db \
|
|
".backup /opt/rfc-app/backend/data/backup-$(date +%F).db"
|
|
```
|
|
|
|
### Restart
|
|
```bash
|
|
sudo systemctl restart rfc-app
|
|
```
|
|
|
|
### Rollback
|
|
```bash
|
|
sudo -u rfc-app git -C /opt/rfc-app checkout <prior-commit>
|
|
sudo systemctl restart rfc-app
|
|
```
|
|
|
|
---
|
|
|
|
## New Session Prompt
|
|
|
|
Paste the following into a new Claude session to continue development:
|
|
|
|
---
|
|
|
|
> I'm working on the **Wiggleverse RFC App** — a FastAPI + SQLite + React + Vite application deployed at `ohm.wiggleverse.org` on a GCP e2-small VM (`ohm-rfc-app` in the `wiggleverse-ohm` project; separate from the `gitea` VM in `wiggleverse` that runs Gitea at `git.wiggleverse.org`). The app is the primary interface for the Open Human Model (OHM) RFC working group.
|
|
>
|
|
> **Stack:**
|
|
> - Backend: Python 3.11, FastAPI, uvicorn (single process), SQLite WAL mode
|
|
> - Frontend: React 19, Vite 8, Tiptap 3 (rich text editor), React Router 7
|
|
> - Infrastructure: nginx (static SPA + API proxy), systemd, Let's Encrypt TLS
|
|
> - Git backend: Gitea at `git.wiggleverse.org`, bot service account `rfc-bot` is the only Git writer
|
|
> - Email: Google Workspace SMTP relay (`smtp-relay.gmail.com`), From `notifications@wiggleverse.org`
|
|
>
|
|
> **Key invariants:**
|
|
> - The bot (`backend/app/bot.py`) is the only Git writer — every commit carries an `On-behalf-of:` trailer and audits to the `actions` table
|
|
> - Git is truth; `cached_*` SQLite tables are written only by the Gitea webhook receiver or the 5-minute background reconciler
|
|
> - Single process, single SQLite file — never use multiple uvicorn workers
|
|
> - 10 append-only schema migrations in `backend/migrations/`
|
|
>
|
|
> **Deployment:**
|
|
> - SSH (IAP-only): `gcloud compute ssh ohm-rfc-app --zone=us-central1-a --project=wiggleverse-ohm --tunnel-through-iap`
|
|
> - Code at `/opt/ohm-rfc-app/` on the VM, owned by `ohm-rfc-app` system user; tracks a detached release **tag**
|
|
> - The VM's git origin is `git.benstull.org/benstull/rfc-app` — a *different* Gitea from the release one (`git.wiggleverse.org`); push `main` + the new tag to the `benstull` remote before deploying
|
|
> - `.env` at `/opt/ohm-rfc-app/backend/.env` (mode 0600)
|
|
> - Frontend built **on the VM** (Node 20 is installed there) with `npm run build` directly into `/opt/ohm-rfc-app/frontend/dist/`
|
|
> - Restart to deploy: `sudo systemctl restart ohm-rfc-app`
|
|
> - Migrations run automatically on startup
|
|
>
|
|
> **Source is at `~/git/rfc-app/`.**
|
|
>
|
|
> The v1 build is complete (8 slices, 125 passing integration tests). I want to [DESCRIBE WHAT YOU WANT TO DO — e.g. "add X feature from the §19.2 backlog" or "deploy the current version to the VM"].
|