Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Frontend demo context (public Vite variables)
VITE_DEMO_PATIENT_ID=<seeded-patient-id>
VITE_DEMO_USER_ID=<seeded-user-id>
# Frontend environment (public Vite variables)
# Auth client uses the root host; REST API calls use the /api path.
VITE_AUTH_BASE_URL=http://localhost:3001
VITE_API_BASE_URL=http://localhost:3001/api

# Feature flags (baked in at build time by Vite)
# Defaults are safe — omit these lines to use the defaults shown below.
VITE_DEMO_MODE=true # true = demo auth active (default). false = unsupported until JWT is added.
VITE_PIPELINE_ENABLED=true # true = AI document pipeline active (default). false = upload page disabled.
VITE_INTEGRATIONS_ENABLED=false # false = no outbound integration calls (default). Reserved for future use.
11 changes: 6 additions & 5 deletions docs/runbook/postgres-backup-and-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,12 @@ ssh -i "$KEY_PATH" "$ADMIN_USER"@"$STATIC_IP" \

The script auto-selects the most recent backup, prompts for confirmation, drops and recreates the database, restores, and prints row counts. Fill in the [Restore Tested Once](#restore-tested-once-h-010-pre-launch-confirmation) table below.

After the restore test passes, the app can be started for the first time:
After the restore test passes, fill in the [Restore Tested Once](#restore-tested-once-h-010-pre-launch-confirmation) table, commit the change, then copy and run the deploy script (single entry point — handles build, migrate, seed, and systemd start):

```bash
scp -i "$KEY_PATH" infra/deploy.sh "$ADMIN_USER"@"$STATIC_IP":/tmp/deploy.sh
ssh -i "$KEY_PATH" "$ADMIN_USER"@"$STATIC_IP" \
"sudo systemctl start havenhold-app"
"sudo bash /tmp/deploy.sh"
```

## D0 Verification Checklist (H-007 — infra only)
Expand All @@ -128,7 +129,7 @@ Complete these during the H-010 initial deploy, after app code is on the server
- [ ] `npx prisma db seed` completed — demo patient/user rows exist.
- [ ] First manual backup written to `/var/backups/havenhold/` with non-zero size.
- [ ] `postgres-restore.sh` run against the most recent backup; row-count validation passed.
- [ ] [Restore Tested Once](#restore-tested-once-h-010-pre-launch-confirmation) table filled in and committed.
- [ ] [Restore Tested Once](#restore-tested-once-h-010-pre-launch-confirmation) table filled in and committed **before** running `infra/deploy.sh` for the first time.

## Evidence Commands

Expand All @@ -149,7 +150,7 @@ sudo ss -tulpen | grep 5432
sudo ufw status verbose | grep 5432 || echo "No 5432 rule — correct"

# Lightsail firewall (run locally, not on host)
aws lightsail get-instance-port-states --instance-name havenhold-app-01 \
aws lightsail get-instance-port-states --instance-name havenhold-api-01 \
| grep -i fromport | grep 5432 || echo "No 5432 Lightsail rule — correct"

# Backup directory and files
Expand Down Expand Up @@ -228,7 +229,7 @@ BACKUP_FILE=/var/backups/havenhold/havenhold_20260528T023001Z.sql \

- **`postgres-setup.sh` failed mid-way:** Re-run — the script is idempotent.
- **pg_dump fails in cron:** Check `/var/log/havenhold-backup.log`. Common causes: disk full (`df -h`), PostgreSQL service stopped (`systemctl status postgresql`). Resolve and run manually to verify.
- **`postgres-restore.sh` fails at DROP — active connections persist:** Stop the application first (`sudo systemctl stop havenhold-app`), then retry.
- **`postgres-restore.sh` fails at DROP — active connections persist:** Stop the application first (`sudo systemctl stop havenhold-api`), then retry.
- **Restore SQL errors (`ON_ERROR_STOP` exits):** The backup may be corrupt or from an incompatible schema version. Try the previous day's backup from `/var/backups/havenhold/`.
- **`pg_hba.conf` conflict on a partially provisioned host:** The validation step at the end of `postgres-setup.sh` catches this. Inspect the file manually (`sudo cat /etc/postgresql/16/main/pg_hba.conf`) and remove duplicate or conflicting entries before re-running.
- **All local backups lost (disk failure):** No S3 offload exists at D0. Restore the Lightsail instance snapshot from the AWS console, then re-run `postgres-setup.sh` with the original `DB_PASSWORD`. Pending migrations can be replayed with `npx prisma migrate deploy`.
Expand Down
102 changes: 102 additions & 0 deletions infra/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# deploy.sh — build, migrate, seed, and start the Havenhold API.
# Run as a sudoer on the app host. Assumes app code is already checked out.
set -euo pipefail

APP_DIR="${APP_DIR:-/opt/havenhold}"
BRANCH="${BRANCH:-main}"
SERVER_DIR="$APP_DIR/server"
HEALTH_URL="http://127.0.0.1:3001/api/health"
SYSTEMD_UNIT="havenhold-api"
SERVICE_FILE="$APP_DIR/infra/systemd/havenhold-api.service"

log() { echo "[deploy] $*"; }

# ── 1. Prerequisites ──────────────────────────────────────────────────────────
log "1/12 Checking prerequisites"
command -v node >/dev/null 2>&1 || { echo "ERROR: node not found"; exit 1; }
command -v npm >/dev/null 2>&1 || { echo "ERROR: npm not found"; exit 1; }
command -v git >/dev/null 2>&1 || { echo "ERROR: git not found"; exit 1; }
[[ -f "$SERVER_DIR/.env" ]] || { echo "ERROR: $SERVER_DIR/.env not found — create it first"; exit 1; }

# Frontend env vars are baked in at build time — require the .env file and validate
# required vars before building.
FRONTEND_ENV="$APP_DIR/.env"
[[ -f "$FRONTEND_ENV" ]] \
|| { echo "ERROR: $FRONTEND_ENV not found — create it from .env.example before deploying"; exit 1; }
grep -qE '^VITE_AUTH_BASE_URL=.+' "$FRONTEND_ENV" \
|| { echo "ERROR: VITE_AUTH_BASE_URL not set in $FRONTEND_ENV — auth will silently target localhost in production browsers"; exit 1; }
grep -qE '^VITE_API_BASE_URL=.+' "$FRONTEND_ENV" \
|| { echo "ERROR: VITE_API_BASE_URL not set in $FRONTEND_ENV"; exit 1; }

# ── 2. Pull latest code ───────────────────────────────────────────────────────
log "2/12 Pulling $BRANCH"
git -C "$APP_DIR" pull --ff-only origin "$BRANCH"

# ── 3. Install frontend dependencies (needs devDeps for Vite build) ───────────
log "3/12 Installing frontend dependencies"
npm --prefix "$APP_DIR" ci

# ── 4. Build frontend ─────────────────────────────────────────────────────────
log "4/12 Building frontend"
npm --prefix "$APP_DIR" run build

# ── 5. Install backend dependencies (needs tsc, prisma CLI, tsx) ─────────────
log "5/12 Installing backend dependencies"
npm --prefix "$SERVER_DIR" ci

# ── 6. Build backend ──────────────────────────────────────────────────────────
log "6/12 Building backend"
npm --prefix "$SERVER_DIR" run build

# ── 7. Run Prisma migrations ──────────────────────────────────────────────────
log "7/12 Running Prisma migrations"
cd "$SERVER_DIR"
npx prisma migrate deploy || { echo "ERROR: Prisma migration failed — aborting deploy"; exit 1; }

# ── 8. Seed database ──────────────────────────────────────────────────────────
log "8/12 Seeding database (idempotent)"
npx prisma db seed

# ── 9. Restore evidence gate ──────────────────────────────────────────────────
log "9/12 Checking restore evidence gate"
RUNBOOK="$APP_DIR/docs/runbook/postgres-backup-and-recovery.md"
if grep -q '_________________' "$RUNBOOK" 2>/dev/null; then
if [[ "${REQUIRE_RESTORE_EVIDENCE:-false}" == "true" ]]; then
echo "ERROR: Restore Tested Once table in runbook has unfilled entries."
echo " Fill in the table and commit before deploying with REQUIRE_RESTORE_EVIDENCE=true."
exit 1
else
echo "WARN: Restore Tested Once table has unfilled entries."
echo " Complete Step E in the runbook and commit evidence before the first production deploy."
fi
else
log " Restore evidence: OK"
fi

# ── 10. Install systemd unit ──────────────────────────────────────────────────
log "10/12 Installing systemd unit"
if [[ -f "$SERVICE_FILE" ]]; then
cp "$SERVICE_FILE" "/etc/systemd/system/$SYSTEMD_UNIT.service"
systemctl daemon-reload
systemctl enable "$SYSTEMD_UNIT"
else
echo "WARN: $SERVICE_FILE not found — skipping systemd unit install"
fi

# ── 11. Restart service ───────────────────────────────────────────────────────
log "11/12 Restarting $SYSTEMD_UNIT"
systemctl restart "$SYSTEMD_UNIT"

# ── 12. Health check ──────────────────────────────────────────────────────────
log "12/12 Polling health endpoint (30s timeout)"
DEADLINE=$(( $(date +%s) + 30 ))
until curl -sf "$HEALTH_URL" >/dev/null 2>&1; do
if [[ $(date +%s) -ge $DEADLINE ]]; then
echo "ERROR: Health check timed out after 30s — check: journalctl -u $SYSTEMD_UNIT -n 50"
exit 1
fi
sleep 2
done

log "Deploy complete. $SYSTEMD_UNIT is healthy."
20 changes: 20 additions & 0 deletions infra/systemd/havenhold-api.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[Unit]
Description=Havenhold API Server
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=simple
User=havenhold
Group=havenhold
WorkingDirectory=/opt/havenhold/server
EnvironmentFile=/opt/havenhold/server/.env
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=havenhold-api

[Install]
WantedBy=multi-user.target
Loading
Loading