From c9df679ac2fa1f39f08b27a46679609fede2f0ef Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 1 Sep 2026 16:07:21 +0800 Subject: [PATCH] Document the real Docker/`node dist/index.js` production path, durable SQLite, single replica, and Firebase credential options so deploys are not treated as equivalent to `tsx`. --- .env.example | 12 +++++---- CHANGELOG.md | 5 ++++ README.md | 70 ++++++++++++++++++++++++++++++++++++++-------------- package.json | 2 +- 4 files changed, 65 insertions(+), 24 deletions(-) diff --git a/.env.example b/.env.example index 531e551..cb6b79d 100644 --- a/.env.example +++ b/.env.example @@ -9,13 +9,15 @@ PORT=3003 # PARTNER_URL=https://partner-api.endorser.ch # DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch -# Firebase Admin: inline service account JSON (one line). -# If unset, uses Application Default Credentials (e.g. GOOGLE_APPLICATION_CREDENTIALS). +# Firebase Admin (required for WAKEUP_PING and AlertSearch FCM). +# Path 1: inline service account JSON (one line). The application reads this variable. # FIREBASE_SERVICE_ACCOUNT_JSON={"type":"service_account",...} +# Path 2: if unset/empty, Application Default Credentials (ADC). +# ADC may use GOOGLE_APPLICATION_CREDENTIALS; the application does not read that name itself. -# Directory for the SQLite FCM registration database (default: ./data). -# Creates notify.sqlite (plus -wal/-shm while the process is running). +# Durable directory for SQLite: notify.sqlite plus -wal/-shm while running (default: ./data). +# In production this must be persistent storage (Docker: volume on /app/data). # NOTIFY_DATA_DIR=./data -# Set to "test-local" to bypass ethr JWT expiry verification in local dev only. +# Do not set NODE_ENV=test-local in production (bypasses ethr JWT expiry). # NODE_ENV=test-local diff --git a/CHANGELOG.md b/CHANGELOG.md index d755115..e44fd93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.14] - 2026.09.01 +### Changed +- Production runbook: Docker/`node dist/index.js` is the canonical deploy path; document durable SQLite, single replica, Firebase credential options, and smoke checks + + ## [0.1.13] - 2026.08.31 ### Added - AlertSearch scheduler delivers a TimeSafari FCM digest (`type: alert_search`) when a completed run consumed today's JWT and `digest.hasUpdates` is true; WAKEUP_PING and JWT/cursor rules are unchanged diff --git a/README.md b/README.md index d79c658..92ed888 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ pnpm run dev pnpm test ``` -The server starts on `http://localhost:3003` (or the port in `PORT`). Hot-reloads on file changes. +The server starts on `http://localhost:3003` (or the port in `PORT`). Hot-reloads on file changes. `pnpm start` is the same `tsx` entry without watch. These commands are **not** the production Docker path (`node dist/index.js`). On first use, the service creates `NOTIFY_DATA_DIR` (default `./data`) and the SQLite file `notify.sqlite` with the required schema. @@ -66,7 +66,7 @@ Authorization: Bearer ### Alert search retrieval -`retrieveAlertSearch` (not yet wired to the scheduler) GETs: +The daily scheduler runs `retrieveAlertSearch` against: - `{ENDORSER_URL}/api/v2/report/alertSearch` - `{PARTNER_URL}/api/partner/alertSearch` @@ -93,6 +93,8 @@ The **delegated** JWT is sent as `Authorization: Bearer`. Pass independent `endo `NOTIFY_DATA_DIR` defaults to `./data` (relative to the process working directory). The `data/` directory is gitignored. +**Production must keep these files on durable storage** (disk or a Docker volume). A container or VM rebuild that drops `NOTIFY_DATA_DIR` loses FCM registrations, delegated JWT batches, and alertSearch cursors. Back up all three files together when using WAL. + ### Schema (high level) Table `fcm_registrations` holds one row per registered device: @@ -132,16 +134,47 @@ Persist or back up the SQLite files under `NOTIFY_DATA_DIR`: ## Production -Runs TypeScript directly via `tsx` (no compile step). +Canonical production deployment is the **Docker image**: `pnpm build` then `node dist/index.js` (`Dockerfile` `CMD`). That is not the same as the development commands `pnpm run dev` / `pnpm start`, which run TypeScript through `tsx` and are for local development only. -```bash -pnpm install --prod -pnpm start -``` +### Single replica -Ensure `NOTIFY_DATA_DIR` points at a durable location (or accept the default `./data` next to the process cwd). +Run **exactly one Node process / one production replica**. AlertSearch scheduling and its in-flight overlap guard are process-local. There is no distributed scheduler lock. SQLite is a local file. Two processes will double-run AlertSearch and FCM wakeup and can corrupt or fork the database. -Or with Docker: +### Firebase credentials + +Working Firebase Admin credentials are required for **both** `WAKEUP_PING` and AlertSearch FCM (`type: alert_search`). The app initializes Firebase once at process start (`src/services/firebase.ts`). + +Supported paths (in this order): + +1. `FIREBASE_SERVICE_ACCOUNT_JSON` — inline service-account JSON (one line). The application reads this variable. +2. If that variable is unset or empty, **Application Default Credentials**. ADC may use `GOOGLE_APPLICATION_CREDENTIALS` (a file path to a key JSON). **The application does not read `GOOGLE_APPLICATION_CREDENTIALS` itself**; the Google/Firebase ADC stack does. + +Invalid `FIREBASE_SERVICE_ACCOUNT_JSON` prevents the process from starting. Missing ADC typically allows listen/`/health` but FCM sends fail later. + +### Persistent SQLite + +Set `NOTIFY_DATA_DIR` to a durable directory, or keep the Docker default `/app/data` on a **persistent volume**. The service uses: + +- `{NOTIFY_DATA_DIR}/notify.sqlite` +- `{NOTIFY_DATA_DIR}/notify.sqlite-wal` +- `{NOTIFY_DATA_DIR}/notify.sqlite-shm` + +### Environment checklist + +| Variable / constraint | Production requirement | Default if unset | +|---|---|---| +| `PORT` | Optional | `3003` | +| `ENDORSER_URL` | Optional if using production Endorser | `https://api.endorser.ch` | +| `PARTNER_URL` | Optional if using production Partner | `https://partner-api.endorser.ch` | +| `FIREBASE_SERVICE_ACCOUNT_JSON` **or** working ADC | **Required** for FCM (wakeup and AlertSearch) | ADC if JSON unset | +| `NOTIFY_DATA_DIR` | **Durable** path (or volume on `/app/data`) | `./data` (cwd-relative; in Docker that is `/app/data`) | +| `NODE_ENV` | Must **not** be `test-local` (that bypasses ethr JWT expiry) | Docker image sets `production` | +| Replicas | **One** process | Not enforced in code | +| Persistent volume | **Required** for Docker so SQLite survives replace | None unless you pass `-v` | + +Optional: `DEFAULT_ENDORSER_API_SERVER` / `DEFAULT_PARTNER_API_SERVER` are honored only if the corresponding `ENDORSER_URL` / `PARTNER_URL` is unset (`src/env.ts`). + +### Docker (canonical) ```bash docker build --no-cache -t notify-wakeup-api:amd-$NOTIFY_WAKEUP_API_VERSION --platform linux/amd64 . @@ -150,14 +183,15 @@ docker run --env-file notify-wakeup-api.env -p 3003:3003 \ notify-wakeup-api ``` -Mount a volume over `/app/data` (or whatever path you set with `NOTIFY_DATA_DIR`) so the SQLite database is not lost when the container is replaced. +The image runs `node dist/index.js`. Mount a volume at `/app/data` (or set `NOTIFY_DATA_DIR` to another mounted path). Do not scale this container to multiple replicas. -Required environment variables: +### Smoke test + +`/health` only means the HTTP server is up. It does **not** prove Firebase, Endorser, Partner, SQLite durability, or AlertSearch. + +1. **Listening:** `curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3003/health` (or the host/port you published). Expect `200`. +2. **Health body:** `curl -sS http://127.0.0.1:3003/health` → `{"ok":true}`. +3. **Both schedulers are started** from `src/index.ts` (`startScheduler()` then `startAlertSearchScheduler()`) when the process reaches `* Running backend`. Neither scheduler runs a pass on startup; the first pass is on the 5-minute timer. +4. **AlertSearch activity in logs:** look for `[AlertSearchScheduler] Pass started` and `[AlertSearchScheduler] Pass completed in` (or `Pass skipped (already in flight)`). User failures log as `[AlertSearchScheduler] User failed`. FCM wakeup passes log `[Scheduler] Pass started` / `[Scheduler] Pass completed in`. These lines appear after the first interval, not at boot. +5. **SQLite location:** after the process has handled a request or a scheduler pass that opens the DB, confirm `{NOTIFY_DATA_DIR}/notify.sqlite` exists (Docker default: `/app/data/notify.sqlite` on the volume). WAL sidecars may appear while the process is running. -| Variable | Description | -|---|---| -| `FIREBASE_SERVICE_ACCOUNT_JSON` | Inline service account JSON (one line). If unset, falls back to Application Default Credentials. | -| `PORT` | HTTP port (default: `3003`). | -| `ENDORSER_URL` | Endorser API base URL used for auth checks and alertSearch (default: `https://api.endorser.ch`). | -| `PARTNER_URL` | Partner API base URL used for alertSearch (default: `https://partner-api.endorser.ch`). | -| `NOTIFY_DATA_DIR` | Directory for the SQLite database file `notify.sqlite` (default: `./data`). | diff --git a/package.json b/package.json index 3f05b4e..8fbe3fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "notification-wakeup-service", - "version": "0.1.13", + "version": "0.1.14", "private": true, "type": "module", "packageManager": "pnpm@11.4.0",