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.

This commit is contained in:
Jose Olarte III
2026-09-01 16:07:21 +08:00
parent cfc2cdefa3
commit c9df679ac2
4 changed files with 65 additions and 24 deletions
+7 -5
View File
@@ -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
+5
View File
@@ -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
+52 -18
View File
@@ -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 <current-user-JWT>
### 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`). |
+1 -1
View File
@@ -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",