8.4 KiB
Go-Live Runbook — HQ ops console (apps/hq)
⚠️ SUPERSEDED (2026-07-21, D39). HQ is live at
https://hq.simssoftware.in— Docker on the existing EC2 box against the sharedaccurate-postgrescontainer, not the RDS + GoDaddy +.complan below. For the actual setup, commands, and paths, usedeploy/PATHS-AND-NAMES.mdinstead. This file is kept for its Postgres-engine background (§7) but its go-live steps (§0–§6, §6b) do not reflect what shipped.
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. Engine: SQLite for dev, Postgres in production via DATABASE_URL
(D19 — the dual-engine switch is delivered; see §7).
0. The decided target (D20)
| Piece | Decision |
|---|---|
| URL | https://hq.simssoftware.com (subdomain of the company domain) |
| DNS | GoDaddy — simssoftware.com stays at the registrar; one A record added |
| Server | The existing company AWS EC2 — HQ gets its own section on the box (/opt/sims-hq), beside whatever else runs there |
| TLS | Caddy reverse proxy (auto Let's Encrypt) — or a server block in the box's existing nginx if one already fronts other sites |
| Process | pm2, started from the repo root |
| Prod data | Born in RDS Postgres — provision RDS first, set DATABASE_URL, THEN run the APEX cutover import straight into it. Sequencing: database first, app section second, DNS/TLS last. |
GoDaddy DNS (5 minutes): My Products → simssoftware.com → DNS → Add record:
Type A, Name hq, Value <EC2 Elastic IP>, TTL 600 (raise to 1h once stable).
Nothing else on the domain changes; existing www/mail records are untouched.
What you provide (the only blockers)
- 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).
- A GitHub repo — so the box can pull the code (or we copy a build artifact).
- 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
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-hq` + push
cd /opt/sims-hq
# Node 20+ (nvm or NodeSource). Then:
npm install
2. Configure
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
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). Start it FROM THE REPO ROOT:
# the DB path resolves to ./data/hq.db against the process CWD, so a pm2 app
# started from apps/hq would boot an empty database in apps/hq/data instead.
npm i -g pm2
env $(grep -v '^#' apps/hq/.env | xargs) pm2 start apps/hq/dist/server.cjs --name sims-hq
pm2 save && pm2 startup
Owner login on first boot is admin@sims.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:
# /etc/caddy/Caddyfile
hq.simssoftware.com {
reverse_proxy localhost:5182
}
# If the box already runs nginx for other sites, use a server block instead:
# server {
# listen 443 ssl; server_name hq.simssoftware.com;
# # certbot --nginx -d hq.simssoftware.com issues the cert
# location / { proxy_pass http://127.0.0.1:5182; proxy_set_header Host $host;
# proxy_set_header X-Forwarded-For $remote_addr; }
# }
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)
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, thennpx tsx apps/hq/scripts/gmail-connect.tsand 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
clientcost-allocation tag, setAWS_*. The scheduler pulls monthly. - Your 300 clients: export
clients.csv+invoices.csvfrom APEX intoimport-drop/, then run the importer (verification report first,--commitafter you approve).
6b. Funnel slice knobs (D16 — set these at go-live)
share.base_urlsetting (required before quote follow-ups can email): set it to the public origin, i.e.https://hq.simssoftware.com, via the settings API or a one-line SQL insert intosetting. The escalating quote follow-up refuses to send without it — a client email carrying a dead relative link is worse than no email.quote.followup.policysetting:manual(default — nudges queue on the dashboard and a person clicks Send) orauto(client emails go out unattended after each daily scan). Start onmanual; flip toautoonce the letter text has been seen in anger.- Reminder cadences are dated rows, not env vars:
reminder_scheduleships seeded withquote_followupat 3/7/14 days andinvoice_overdueat 7/15/30 days past invoice date. To change a cadence (or the follow-up email wording), insert a new dated row — effective from a business date, no release, no restart. - Employee accounts: after first boot, open Employees (owner-only nav) and create the real manager/staff logins. Staff see only their own pipeline and reminder queue; owner/manager see everything. Deactivating someone kills their session immediately.
- Account owners: assign an owner on each client (Client 360 → Account owner) so leads route to the right person's queue; unassigned rows stay visible to owner/manager.
7. Production database — RDS Postgres (do this FIRST; D19 delivered the switch)
The dual-engine work is done (D19): the app selects its engine from DATABASE_URL —
unset → SQLite (dev), set → Postgres. Migrations apply automatically from
migrations-pg.ts on boot. Go-live order (so data is born in Postgres, never migrated):
- Provision RDS PostgreSQL 17 (db.t4g.micro is plenty), same VPC as the EC2, not publicly accessible; security group allows 5432 from the EC2's group only.
- Create database
hq+ a scopedhqrole; putDATABASE_URL=postgres://hq:<pw>@<rds-endpoint>:5432/hqinapps/hq/.env. - Boot once — migrations run, first-boot seed prints the owner password.
- Then run the APEX cutover UI (stage → verify → commit): the 300 clients import directly into Postgres.
Known nuance (D19): client search is case-sensitive on Postgres until repos move to
LOWER(...) LIKE LOWER(?). SQLite + data/hq.db remains the dev/test path; the §5
sqlite backup script is superseded in prod by RDS automated snapshots (enable 7-day
retention) — keep the S3 dump cron as belt-and-braces via pg_dump if desired.
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.