Wires up the prod deployment per architecture-doc conventions:
- api → nikola.kosherinata.internal, loopback bind 127.0.0.1:42424
(less-common port, registered with SELinux as http_port_t).
- worker → frootmig.kosherinata.internal, no listening port.
- web (static ui/dist + nginx server_name rob.tn) → nikola, with
/api/* reverse-proxied to the loopback API.
- db → existing magrathea cluster via mTLS, hostname-baked DATABASE_URL
rendered into /etc/moments/{api,worker}.env at deploy time.
Cert rotation: step-ca renews host certs every 24h; .path units watch
/etc/pki/tls/misc/<host>.pem and trigger systemctl restart of the
relevant service. Both binaries hold cert state in rustls and read
once at startup, so restart is the right reload semantics.
deploy.sh contract matches the architecture doc: positional env arg,
component list (or `all` / `default`), --dry-run support. Renders
config templates from `pass`, rsyncs over ssh+sudo, runs sysusers /
restorecon / semanage / systemctl / nginx -t idempotently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
moments
Personal activity timeline for rob.tn. Polls public sources (GitHub, Gitea, hg-edge.mozilla.org, bugzilla.mozilla.org), stores raw payloads in Postgres, and serves a reshaped timeline to a static React frontend.
Successor to the now-defunct grenade-events-react, which depended on MongoDB Stitch (retired by MongoDB in September 2022).
Layout
crates/
moments-entities/ # types and DTOs
moments-core/ # ingestion + reshape logic
moments-data/ # postgres adapter + migrations
moments-api/ # axum read-only HTTP API (binary)
moments-worker/ # ingestion daemon (binary)
ui/ # vite + react + swc + ts frontend
asset/ # systemd, nginx, firewalld, manifest.yml
script/deploy.sh
Architectural conventions follow grenade/architecture/generic.md.
Local development
cargo build --workspace
cargo run -p moments-api # serves on 127.0.0.1:8080
cargo run -p moments-worker # one-shot ingest tick (until --interval is wired up)
The API expects a Postgres reachable at DATABASE_URL. For magrathea, that's an mTLS connection using the host cert. For local dev against a throwaway database:
DATABASE_URL=postgres://localhost/moments cargo run -p moments-api
Migrations live in crates/moments-data/migrations/ and run automatically on worker startup. The API connects as moments_ro and never runs migrations — the worker (as moments_rw) is the schema owner.
Deployment
./script/deploy.sh prod all # api + worker + web
./script/deploy.sh prod api worker # subset
./script/deploy.sh prod default # api + web only (worker untouched)
./script/deploy.sh prod all --dry-run
Topology:
| Component | Host | Notes |
|---|---|---|
| api | nikola.kosherinata.internal |
binds 127.0.0.1:42424, fronted by local nginx |
| worker | frootmig.kosherinata.internal |
no listening port; pollers only |
| web | nikola.kosherinata.internal |
static ui/dist/ under /var/www/moments |
| db | magrathea.kosherinata.internal |
postgres mTLS, passwordless |
Postgres roles moments_rw and moments_ro must exist on the primary, with pg_ident.conf mappings in place for nikola.kosherinata.internal → moments_ro and frootmig.kosherinata.internal → moments_rw. See asset/sql/bootstrap-moments.sql and asset/postgres/ident.conf.tmpl.
Secrets resolved by deploy.sh via pass:
github.com/grenade/admin-token— GitHub PAT for events + search APIs (worker only).
Optional, set if needed in worker.env: GITEA_TOKEN, BUGZILLA_API_KEY.
DNS cutover
rob.tn currently resolves to GitHub Pages. After the first successful prod deploy:
- Update Cloudflare DNS for
rob.tnto the WAN IP that frontsnikola(unproxied — see architecture doc §11). - Confirm
curl -fsS https://rob.tn/api/v1/healthzreturnsok. - Add an archival notice to the top of grenade-events-react/readme.md pointing at this repo, and archive the GitHub repo.