RealH is a two-service app: a stateless Node API and a static React SPA. The API requires persistent signing keys (file-based in demo, KMS in production) and, outside demo mode, a Firestore instance.
┌───────────────┐ ┌──────────────────┐
user ─▶ │ CDN / nginx │ ─────▶ │ Static SPA │ (packages/client)
└───────────────┘ └──────────────────┘
│
▼
┌───────────────┐ ┌──────────────────┐
│ API server │ ─────▶ │ Firestore │ (state: users, creds)
│ (Node 20) │ └──────────────────┘
│ │ ─────▶ ┌──────────────────┐
└───────────────┘ │ KMS (GCP/AWS/…) │ (signing key)
└──────────────────┘
The API is stateless once keys live in KMS — scale horizontally behind any load balancer.
- Docker —
docker-compose.ymlat the repo root covers the full local stack. Each service has its own Dockerfile underpackages/*/Dockerfilefor standalone builds. - Cloud Run / Fly / Render — build the server image from
packages/server/Dockerfile; expose port3001; wire env vars per ENVIRONMENT.md. - Static hosts for the client — Cloudflare Pages, Netlify, Vercel, S3+CloudFront, or nginx.
npm run build -w packages/clientemits a plaindist/. - Node —
node packages/server/src/index.jsunder a process manager (systemd, PM2). Put a reverse proxy in front for TLS.
Both Dockerfiles must be built from the repo root so npm workspaces resolve.
# API
docker build -f packages/server/Dockerfile -t realh/server:latest .
# Client (bake the API URL in at build time)
docker build -f packages/client/Dockerfile \
--build-arg VITE_API_URL=https://api.example.com \
-t realh/client:latest .Before pointing traffic at a deployment:
-
DEMO_MODE=falseand Firebase Admin credentials wired via your secret store. -
KEY_MANAGER=kmsand a non-exportable Ed25519 key in your KMS (see SECURITY_ARCHITECTURE.md). -
SERVER_BASE_URLmatches the public URL the API is reachable at — it ends up in every issued credential'sissuer(did:web:<host>). -
CORS_ORIGINSis the explicit allowlist of SPA origins. No wildcards. -
JSON_BODY_LIMITleft at the default (256kb) or tighter. - TLS terminates in front of the API;
app.set('trust proxy', 1)inapp.jsassumes a single proxy hop — adjust if you chain more. -
/.well-known/jwks.jsonand/.well-known/did.jsonare reachable over HTTPS at the root ofdid:web:<host>— relying parties resolve them from there. - Rate limits reviewed for your traffic (
packages/server/src/middleware/rateLimit.js). - Logs go somewhere — the default morgan sink is stdout; aggregate it.
Three data-layer backends ship in this repo, selected by the DATA_LAYER env var (defaults to firestore when DEMO_MODE=false):
DATA_LAYER |
Backend | Requirements | Status |
|---|---|---|---|
memory |
In-process Map |
none | Dev/test only. Loses data on restart. |
firestore |
Google Cloud Firestore | firebase-admin creds + rules deployed | Production-tested. |
postgres |
PostgreSQL 14+ | pg installed + DATABASE_URL + schema applied |
Reference skeleton — not CI-covered yet. |
All three implement the same DataLayer interface defined in packages/server/src/data/interface.js. Adding a new backend = implement the interface and register it in initializeDataLayer.
- Install
pgin the server workspace:npm install pg -w packages/server. - Create a database and apply the schema:
psql "$DATABASE_URL" -f packages/server/src/data/postgres.schema.sql - Set env:
DATA_LAYER=postgres,DATABASE_URL=postgres://....
The Postgres backend carries no CI integration test today — it's a working skeleton, reviewed for shape. Before any production use, run integration tests against a real Postgres (vitest + testcontainers-node is the path we'd take).
In production mode, packages/server/src/data/firestore.js handles persistence. You need:
- A GCP project with Firestore (Native mode) enabled.
- A service account with
roles/datastore.user. - The service account key provided via
FIREBASE_PROJECT_ID+FIREBASE_CLIENT_EMAIL+FIREBASE_PRIVATE_KEY(the last one read from a secret store, not disk). - The Firestore security rules in firestore.rules deployed. The shipped rules deny all client-SDK reads and writes — the RealH server uses firebase-admin which bypasses rules, and no component in this repo accesses Firestore from the browser.
Deploy the rules with the Firebase CLI:
npm install -g firebase-tools
firebase login
firebase use <your-project-id>
firebase deploy --only firestore:rulesRe-deploy after any rule change. Clients authenticate with Firebase Auth and always go through the RealH API; they should never hold the database-admin credentials.
The current file-based key manager loads one key per process. To rotate without downtime:
- Publish the new public key in
/.well-known/jwks.jsonalongside the old one (multi-key JWKS). - Switch signing to the new
kid. - Wait for the longest-lived credential's
validFromwindow to age out, or publish revocation metadata. - Retire the old key.
The KMS adapter (kmsKeyManager.js) is the right place to add multi-key support; track this under a feature issue before production.
GET /healthreturns{ ok: true, mode }— wire to your LB health check and uptime monitor.- Morgan logs requests; pipe to your log stack.
- CodeQL + gitleaks run on every PR via GitHub Actions.
npm audit --audit-level=highfails CI on new high-severity advisories.
The API is stateless, so rollback is: redeploy the previous image. Credentials already signed by a rolled-back key are still valid (the public JWKS has not changed). Only a key rotation is irreversible — plan for it separately from code rollbacks.