Files
rfc-app/deploy/DEPLOY-NEW-SESSION-PROMPT.md
T
Ben Stull 714c2aed86 docs(deploy): correct stale infra facts after GCP name-alignment
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>
2026-06-01 10:15:58 -07:00

16 KiB

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-onlygcloud 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):

# 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:

# 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):

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:

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):

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):

# 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

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

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

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

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)

cd /opt/rfc-app/frontend && sudo -u rfc-app npm ci
sudo -u rfc-app npm run build

7. nginx

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

sudo certbot --nginx -d ohm.wiggleverse.org

9. systemd

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

sudo journalctl -u rfc-app -f
sudo journalctl -u rfc-app -p err

Database backup

sqlite3 /opt/rfc-app/backend/data/rfc-app.db \
  ".backup /opt/rfc-app/backend/data/backup-$(date +%F).db"

Restart

sudo systemctl restart rfc-app

Rollback

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"].