A lightweight push notification relay that forwards notifications to APNs (iOS) and FCM (Android) on behalf of ACP Bridge instances.
Bridge instances run on user machines. Distributing APNs .p8 keys or FCM service account credentials to every bridge would be a critical security risk. The relay centralizes credential management while bridges only need the relay URL.
Bridge ──POST /push──→ [ Push Relay Worker ] ──→ APNs / FCM
│
┌───────────┴───────────┐
│ KV: DEVICE_TOKENS │ relay_token → [devices]
│ KV: AUTH_TOKENS │ cached JWT / OAuth2 token
└───────────────────────┘
│
Cron (every 45 min) refreshes auth tokens
Two logical workers in one script:
- Push Worker (
fetchhandler) – Routes HTTP requests, reads cached tokens from KV - Token Worker (
scheduledhandler) – Cron-triggered, refreshes APNs JWT and FCM OAuth2 token
Health check. Returns { "ok": true, "status": "healthy" }.
Register a device for push notifications.
{
"relay_token": "<bridge auth_token, ≥32 chars>",
"device_token": "<APNs or FCM device token>",
"platform": "ios" | "android",
"bundle_id": "com.example.app" // optional
}Unregister a device.
{
"relay_token": "<bridge auth_token>",
"device_token": "<device token to remove>"
}Send a push notification to all devices registered under a relay token.
{
"relay_token": "<bridge auth_token>",
"title": "New Tool Request",
"body": "Agent wants to run 'rm -rf /'"
}Response:
{
"ok": true,
"results": [
{ "platform": "ios", "status": "sent" },
{ "platform": "android", "status": "sent" }
]
}- Node.js ≥ 18
- Wrangler CLI (
npm i -g wrangler) - A Cloudflare account
cd cf-push-relay
npm installwrangler kv namespace create DEVICE_TOKENS
wrangler kv namespace create AUTH_TOKENSCopy the IDs into wrangler.toml.
# APNs (iOS)
wrangler secret put APNS_PRIVATE_KEY # paste .p8 file contents
wrangler secret put APNS_KEY_ID # 10-char key ID
wrangler secret put APNS_TEAM_ID # 10-char team ID
# FCM (Android)
wrangler secret put FCM_PRIVATE_KEY # RSA key from service account JSON
wrangler secret put FCM_CLIENT_EMAIL # service account emailEdit wrangler.toml:
[vars]
APNS_BUNDLE_ID = "com.yourapp.bundle"
APNS_SANDBOX = "true" # "false" for production
FCM_PROJECT_ID = "your-firebase-project-id"npm run deploy
# or: wrangler deployPushing to main automatically runs tests and deploys to Cloudflare Workers.
Required GitHub Secrets (Settings → Secrets and variables → Actions):
| Secret | Description | How to get it |
|---|---|---|
CLOUDFLARE_API_TOKEN |
Scoped API token for Workers | Cloudflare Dashboard → My Profile → API Tokens → Create Token → use "Edit Cloudflare Workers" template. Scope to your account and aptov.com zone only. |
CLOUDFLARE_ACCOUNT_ID |
Your Cloudflare account identifier | Cloudflare Dashboard → any domain → right sidebar under Account ID (32-char hex). |
Security note: Use the "Edit Cloudflare Workers" token template — it grants only Workers write + Account read permissions. Restrict the token to your specific account and zone so a leaked token cannot affect other resources.
# Create .dev.vars for local secrets
cat > .dev.vars << 'EOF'
APNS_PRIVATE_KEY=...
APNS_KEY_ID=ABC1234567
APNS_TEAM_ID=XYZ9876543
FCM_PRIVATE_KEY=...
FCM_CLIENT_EMAIL=firebase@project.iam.gserviceaccount.com
EOF
npm run devnpm test # run all tests
npm run test:watch # watch mode
npm run typecheck # TypeScript type checkingAPNs (iOS): The device token appears in the URL path:
POST https://api.push.apple.com/3/device/<DEVICE_TOKEN>
Authorization: bearer <JWT>
The JWT identifies the publisher (Team ID + Key ID). Apple uses the device token to look up which physical device to deliver to via its persistent APNs connection.
FCM (Android): The device token appears in the request body:
POST https://fcm.googleapis.com/v1/projects/<PROJECT>/messages:send
{
"message": {
"token": "<DEVICE_TOKEN>",
"notification": { "title": "...", "body": "..." }
}
}The OAuth2 bearer token identifies the publisher (service account). Google uses the device token to route via its persistent GCM connection to the device.
Key insight: Auth credentials (JWT/OAuth2) = "who is sending". Device tokens = "where to deliver". These are completely separate concerns.
- APNs/FCM credentials never leave the relay (stored as Cloudflare Secrets)
- Bridge instances only know the relay URL, never the push credentials
- Each bridge's
auth_token(from QR pairing) serves as itsrelay_token - Device tokens are isolated per
relay_tokenin KV (no cross-bridge access) - Stale device tokens are automatically cleaned up when APNs/FCM reports them invalid