docs(hq): go-live runbook, env template and S3 backup script

Turnkey deployment for the AWS box: documented env vars, pm2/systemd run,
HTTPS reverse-proxy, nightly SQLite->S3 backup with rotation, and the Postgres
switch note. No secrets; unblocks go-live the moment the server/creds arrive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 1 week ago
parent 5fe86f40ba
commit 4559958fa4

3
.gitignore vendored

@ -5,6 +5,9 @@ dist/
*.log
.env
.env.*
!.env.example
!*/.env.example
!**/.env.example
.gstack/
apps/store-server/data/
apps/hq/data/

@ -0,0 +1,34 @@
# SiMS HQ console — environment. Copy to `.env` (gitignored) and fill in.
# Nothing here is required to BOOT (the app runs with none set), but each unlocks
# a capability. Never commit the filled-in file.
# --- Core ---
# Where the SQLite database lives (created on first boot). On the AWS box use an
# absolute path on a backed-up volume, e.g. /var/lib/sims-hq/data
HQ_DATA_DIR=/var/lib/sims-hq/data
# Port the console listens on (default 5182).
HQ_PORT=5182
# --- Security (set BEFORE first real login) ---
# Server-held pepper mixed into every password/PIN hash. Without it, a stolen DB
# can be brute-forced. Generate once, keep secret, NEVER change after users exist:
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
SIMS_PIN_PEPPER=
# Encrypts the stored Gmail refresh token (AES-256-GCM). 64 hex chars:
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
HQ_SECRET_KEY=
# --- Gmail sending (quotations, reminders) ---
# From a Google Cloud project (OAuth client, app published to Production). After
# setting these, run: npx tsx apps/hq/scripts/gmail-connect.ts (one-time consent).
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# --- AWS cost-per-client (HQ-3) ---
# A read-only IAM user with Cost Explorer access. When both are set, the scheduler
# pulls monthly cost grouped by the 'client' cost-allocation tag.
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
# --- Postgres (go-live; not yet wired — see docs/DEPLOY-HQ.md) ---
# HQ_PG_URL=postgres://hq:PASSWORD@your-rds-endpoint:5432/hq

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Nightly backup of the HQ SQLite database to S3 (and a local rotation).
# Usage: HQ_DATA_DIR=/var/lib/sims-hq/data S3_BUCKET=s3://your-bucket/hq-backups ./backup-hq.sh
# Cron (2am daily): 0 2 * * * /opt/sims-hq/apps/hq/scripts/backup-hq.sh >> /var/log/sims-hq-backup.log 2>&1
set -euo pipefail
DATA_DIR="${HQ_DATA_DIR:-/var/lib/sims-hq/data}"
DB="$DATA_DIR/hq.db"
STAMP="$(date +%Y%m%d-%H%M%S)"
OUT="$DATA_DIR/backups"
mkdir -p "$OUT"
if [ ! -f "$DB" ]; then echo "no db at $DB"; exit 1; fi
# Consistent snapshot even while the app is running (WAL-safe).
sqlite3 "$DB" ".backup '$OUT/hq-$STAMP.db'"
gzip -f "$OUT/hq-$STAMP.db"
echo "local backup: $OUT/hq-$STAMP.db.gz"
# Off-box copy (recommended). Requires awscli + an IAM role/keys with s3:PutObject.
if [ -n "${S3_BUCKET:-}" ]; then
aws s3 cp "$OUT/hq-$STAMP.db.gz" "$S3_BUCKET/hq-$STAMP.db.gz"
echo "uploaded: $S3_BUCKET/hq-$STAMP.db.gz"
fi
# Keep the last 14 local snapshots.
ls -1t "$OUT"/hq-*.db.gz 2>/dev/null | tail -n +15 | xargs -r rm -f
echo "backup ok"

@ -0,0 +1,89 @@
# Go-Live Runbook — HQ ops console (`apps/hq`)
How to put the internal console on your AWS box for real use. The store product is
unaffected (it stays local, D14); this is the vendor-side cloud app doc 11 always
prescribed. Today the engine is SQLite; the Postgres switch is called out where it lands.
## What you provide (the only blockers)
1. **A server** — a small AWS EC2 instance (t3.small is plenty for 5 users), Ubuntu 22.04+,
with a domain or Elastic IP and HTTPS (via a reverse proxy — step 4).
2. **A GitHub repo** — so the box can pull the code (or we copy a build artifact).
3. Later, per capability: a Google Cloud OAuth app (Gmail), a read-only AWS IAM user
(cost data), and your **APEX CSV export** (the 300 clients).
## 1. Get the code on the box
```bash
sudo mkdir -p /opt/sims-hq && sudo chown $USER /opt/sims-hq
git clone <your-repo-url> /opt/sims-hq # after `gh repo create sims-next` + push
cd /opt/sims-hq
# Node 20+ (nvm or NodeSource). Then:
npm install
```
## 2. Configure
```bash
cp apps/hq/.env.example apps/hq/.env
# Fill at minimum SIMS_PIN_PEPPER and HQ_SECRET_KEY (generators are in the file).
# Set HQ_DATA_DIR to a path on a backed-up volume, e.g. /var/lib/sims-hq/data.
```
`SIMS_PIN_PEPPER` **must be set before the first real login** and never changed after
(changing it invalidates every password). `HQ_SECRET_KEY` must be set before connecting
Gmail.
## 3. Build + run under a process manager
```bash
cd apps/hq-web && npm run build && cd ../.. # builds the web UI the server serves
# First boot prints a one-time owner password — capture it from the logs.
env $(grep -v '^#' apps/hq/.env | xargs) node apps/hq/dist/server.cjs # smoke test, Ctrl-C
# Keep it running with pm2 (or a systemd unit):
npm i -g pm2
cd apps/hq && pm2 start "npm start" --name sims-hq && pm2 save && pm2 startup
```
Owner login on first boot is `admin@tecnostac.com` + the printed password (change it
immediately). The Gmail-disconnected banner is expected until step 6.
## 4. Put HTTPS in front (never expose Node directly)
Reverse-proxy 443 → `localhost:5182` with Caddy (simplest) or nginx + certbot:
```
# Caddyfile
hq.yourdomain.com {
reverse_proxy localhost:5182
}
```
Lock the EC2 security group so only 443 is public; 5182 stays localhost-only. The public
`/share/:token` links are served through this same proxy — that's intended (they're the
one unauthenticated route, token-guarded, rate-limited, revocable).
## 5. Backups (do this on day one)
```bash
chmod +x apps/hq/scripts/backup-hq.sh
# Nightly to S3 + 14-day local rotation:
crontab -e
0 2 * * * HQ_DATA_DIR=/var/lib/sims-hq/data S3_BUCKET=s3://your-bucket/hq-backups /opt/sims-hq/apps/hq/scripts/backup-hq.sh >> /var/log/sims-hq-backup.log 2>&1
```
Do a **restore drill** once: copy a backup to a scratch box and boot the app against it.
## 6. Connect the capabilities (each optional, each unlocks a feature)
- **Gmail** (sending): create a Google Cloud OAuth client, publish the app to Production,
set `GOOGLE_CLIENT_ID/SECRET` + `HQ_SECRET_KEY`, then `npx tsx apps/hq/scripts/gmail-connect.ts`
and grant consent as the company mailbox. The banner clears; auto-reminders start sending.
- **AWS cost per client**: create a read-only IAM user with Cost Explorer access, tag each
client's cloud resources with a `client` cost-allocation tag, set `AWS_*`. The scheduler
pulls monthly.
- **Your 300 clients**: export `clients.csv` + `invoices.csv` from APEX into `import-drop/`,
then run the importer (verification report first, `--commit` after you approve).
## 7. The Postgres switch (before or at go-live)
Decision D15 locks Postgres as the production engine. It is a **dedicated engineering task,
not a config flag** — the repositories move from synchronous SQLite to async Postgres
behind the same interfaces, validated against your RDS instance. Provide the RDS endpoint +
a dedicated empty `hq` database and a scoped user (`HQ_PG_URL`), and it is built and tested
against that target, then the 300 clients import directly into Postgres — data never
migrates engines mid-life. Until then the app runs correctly on SQLite with the backups above.
## Health & operations
- `GET /api/health``{ ok: true }` for your uptime monitor.
- Logs: `pm2 logs sims-hq`. The scheduler runs on boot and every 6h (reminders, bounces,
AWS pull when configured).
- Every mutation is audit-logged in the DB; the public share route and previews write nothing.
Loading…
Cancel
Save