Jose Olarte III caeca10910 Fix Endorser alertSearch pagination so the next beforeId is the max of per-bucket minima, not the global min.
Treat a full plansNearby page as incomplete because the server orders that bucket by rowid while filtering jwtId.
2026-08-27 16:34:36 +08:00
2026-07-02 17:14:11 +08:00

A lightweight Express service that schedules and sends Firebase Cloud Messaging (FCM) push notifications to wake up registered devices.

Device registrations are stored in a local SQLite database (not JSON).

Dev

cp .env.example .env

Edit .env — set FIREBASE_SERVICE_ACCOUNT_JSON. Here is one way to generate the contents: cat your-downloaded-key.json | jq -c .

Optionally set ENDORSER_URL / PARTNER_URL if you are not using the production Endorser (https://api.endorser.ch) and Partner (https://partner-api.endorser.ch) hosts.

Optionally set NOTIFY_DATA_DIR if you want the SQLite database somewhere other than ./data.

pnpm install
pnpm run dev
pnpm test

The server starts on http://localhost:3003 (or the port in PORT). Hot-reloads on file changes.

On first use, the service creates NOTIFY_DATA_DIR (default ./data) and the SQLite file notify.sqlite with the required schema.

Authentication

POST /notifications/register and POST /notifications/refresh require a Bearer JWT. After local JWT verification, the service checks the token with Endorser (GET /api/report/rateLimits on ENDORSER_URL). Registration and refresh continue only if Endorser accepts the JWT.

PUT /notifications/alert-authorization uses the same current-user Bearer JWT + Endorser check. It does not accept the testMode local bypass. The 100 delegated JWTs in the body are stored credentials, not the request authenticator.

Local notification test bypass: send testMode: true in the JSON body and omit the Authorization header. The request skips JWT and Endorser checks and uses a synthetic local test user, same as before. This applies to register/refresh only.

Set NODE_ENV=test-local in .env to bypass ethr JWT expiry verification during local development (this is separate from the testMode bypass above).

Alert authorization

PUT /notifications/alert-authorization

Authorization: Bearer <current-user-JWT>
{
  "batchId": "client-batch-id",
  "timezone": "America/Denver",
  "jwts": [
    {
      "sequence": 0,
      "day": "2026-08-27",
      "nbf": 1756270800,
      "exp": 1756357200,
      "jwt": "eyJ..."
    }
  ]
}

timezone is the IANA zone used when minting the 100 validity windows; it is stored as batch metadata, not live device-timezone tracking. A successful call replaces that user's previous unused JWTs atomically. Passkey (did:peer) identities cannot mint this batch and receive DELEGATED_JWT_UNSUPPORTED_IDENTITY.

Alert search retrieval

retrieveAlertSearch (not yet wired to the scheduler) GETs:

  • {ENDORSER_URL}/api/v2/report/alertSearch
  • {PARTNER_URL}/api/partner/alertSearch

The delegated JWT is sent as Authorization: Bearer. Pass independent endorserAfterId / partnerAfterId ULIDs, or omit them on first run. Nearby search uses the alertSearch bbox (minLocLat, maxLocLat, minLocLon, maxLocLon).

Storage

Database location

Path Description
{NOTIFY_DATA_DIR}/notify.sqlite Primary SQLite database (default dir: ./data)
{NOTIFY_DATA_DIR}/notify.sqlite-wal WAL journal (present while the process is running)
{NOTIFY_DATA_DIR}/notify.sqlite-shm Shared-memory file used with WAL mode

NOTIFY_DATA_DIR defaults to ./data (relative to the process working directory). The data/ directory is gitignored.

Schema (high level)

Table fcm_registrations holds one row per registered device:

  • Identity: id, user_id, device_id, fcm_token, platform
  • Flags: test_mode
  • Timestamps: created_at, updated_at, last_notified_at

Unique on (user_id, device_id). Indexes also exist on user_id, device_id, fcm_token, and (user_id, fcm_token).

Tables alert_authorization_batches and alert_authorization_jwts hold a user's delegated notification-JWT inventory (separate from device registration):

  • Batch: id, user_id (authenticated DID), batch_id, timezone (IANA name at mint time), created_at
  • JWT: batch_pk, sequence, day (YYYY-MM-DD), jwt, nbf, exp, status (unused / consumed), consumed_at, timestamps

Unique on (batch_pk, sequence) and on (user_id, day) for unused rows. Indexes also exist on (user_id, status), (user_id, day), and batch_pk.

The schema is created automatically on startup if the database or tables do not already exist. New tables are added with CREATE TABLE IF NOT EXISTS; existing fcm_registrations rows are not migrated or altered.

JSON → SQLite

There is no automatic migration from the old JSON file (fcm-tokens.json). That format is no longer used. If you still have a local fcm-tokens.json from earlier development, it is ignored; re-register devices or import data manually if you need it.

Backup

Persist or back up the SQLite files under NOTIFY_DATA_DIR:

  1. Prefer stopping the service, then copy notify.sqlite (and any -wal / -shm sidecars if present).
  2. Or, while the service is running, copy all three files (notify.sqlite, -wal, -shm) together so the backup stays consistent under WAL mode.
  3. For Docker, mount a volume at the data directory (or set NOTIFY_DATA_DIR to a mounted path) so registrations survive container recreation.

Production

Runs TypeScript directly via tsx (no compile step).

pnpm install --prod
pnpm start

Ensure NOTIFY_DATA_DIR points at a durable location (or accept the default ./data next to the process cwd).

Or with Docker:

docker build --no-cache -t notify-wakeup-api:amd-$NOTIFY_WAKEUP_API_VERSION --platform linux/amd64 .
docker run --env-file notify-wakeup-api.env -p 3003:3003 \
  -v notify-wakeup-data:/app/data \
  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.

Required environment variables:

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).
S
Description
No description provided
Readme
1.1 MiB
Languages
TypeScript 98.1%
Dockerfile 1.9%