Skip to content

aptove/push-relay

Repository files navigation

Push Relay – Cloudflare Worker

A lightweight push notification relay that forwards notifications to APNs (iOS) and FCM (Android) on behalf of ACP Bridge instances.

Why a Relay?

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.

Architecture

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 (fetch handler) – Routes HTTP requests, reads cached tokens from KV
  • Token Worker (scheduled handler) – Cron-triggered, refreshes APNs JWT and FCM OAuth2 token

API

GET /health

Health check. Returns { "ok": true, "status": "healthy" }.

POST /register

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
}

DELETE /register

Unregister a device.

{
  "relay_token": "<bridge auth_token>",
  "device_token": "<device token to remove>"
}

POST /push

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" }
  ]
}

Setup

Prerequisites

1. Install dependencies

cd cf-push-relay
npm install

2. Create KV namespaces

wrangler kv namespace create DEVICE_TOKENS
wrangler kv namespace create AUTH_TOKENS

Copy the IDs into wrangler.toml.

3. Set secrets

# 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 email

4. Configure variables

Edit wrangler.toml:

[vars]
APNS_BUNDLE_ID = "com.yourapp.bundle"
APNS_SANDBOX   = "true"     # "false" for production
FCM_PROJECT_ID = "your-firebase-project-id"

5. Deploy

npm run deploy
# or: wrangler deploy

6. CI/CD (GitHub Actions)

Pushing 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.

7. Local development

# 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 dev

Testing

npm test           # run all tests
npm run test:watch # watch mode
npm run typecheck  # TypeScript type checking

How Device Token Addressing Works

APNs (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.

Security Model

  • 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 its relay_token
  • Device tokens are isolated per relay_token in KV (no cross-bridge access)
  • Stale device tokens are automatically cleaned up when APNs/FCM reports them invalid

About

CloudFlare Push Relay Worker

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published