Co-authored-by: Cursor <cursoragent@cursor.com>
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 if you are not using the default production Endorser API (https://api.endorser.ch).
Optionally set NOTIFY_DATA_DIR if you want the SQLite database somewhere other than ./data.
pnpm install
pnpm run dev
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.
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.
Set NODE_ENV=test-local in .env to bypass ethr JWT expiry verification during local development (this is separate from the testMode bypass above).
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).
The schema is created automatically on startup if the database or tables do not already exist.
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:
- Prefer stopping the service, then copy
notify.sqlite(and any-wal/-shmsidecars if present). - Or, while the service is running, copy all three files (
notify.sqlite,-wal,-shm) together so the backup stays consistent under WAL mode. - For Docker, mount a volume at the data directory (or set
NOTIFY_DATA_DIRto 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 on register/refresh (default: https://api.endorser.ch). |
NOTIFY_DATA_DIR |
Directory for the SQLite database file notify.sqlite (default: ./data). |