Skip to content

Self-hosted push notifications for iOS, powered by Cloudflare Workers.

License

Notifications You must be signed in to change notification settings

jonesphillip/paje

Repository files navigation

Paje

Self-hosted push notifications for iOS, powered by Cloudflare Workers.

Push Notification

What is Paje?

Paje is a self-hosted push notification service. Deploy a Cloudflare Worker as your server, build the iOS app with your own Apple Developer account, and send notifications with a single HTTP request.

  • Fully self-hosted — your server, your APNs credentials, your data
  • Simple HTTP APIcurl -d '{"body":"Hello"}' https://your-server.workers.dev/my-topic
  • Dashboard — see subscriptions, messages, and delivery stats
  • Rich notifications — images, action buttons, click URLs

Architecture

Architecture

Component Purpose
Worker HTTP routing, auth, serves dashboard, publish API
TopicDO Per-topic Durable Object — stores devices and messages in SQLite, sends APNs push, broadcasts updates over WebSocket
iOS App Subscribe to topics, receive push notifications, view message history

Prerequisites

Setup

1. Clone and install

git clone https://github.com/jonesphillip/paje.git
cd paje
npm install

2. Create APNs key

  1. Go to Apple Developer > Keys
  2. Create a new key with Apple Push Notifications service (APNs) enabled
  3. Download the .p8 file and note the Key ID
  4. Note your Team ID from Membership Details

3. Configure secrets

# Base64-encode your .p8 key (downloaded in step 2)
cat AuthKey_XXXXXXXXXX.p8 | base64 | pbcopy

# Set secrets on Cloudflare
npx wrangler secret put APNS_KEY_P8      # paste the base64 string
npx wrangler secret put APNS_KEY_ID      # 10-char ID shown when you created the key (e.g. ABC123DEFG)
npx wrangler secret put APNS_TEAM_ID     # found at developer.apple.com > Membership Details (e.g. 9A8B7C6D5E)
npx wrangler secret put APP_BUNDLE_ID    # the bundle ID you'll use in Xcode (e.g. com.yourname.paje)
npx wrangler secret put APNS_SANDBOX     # set to "true" for dev/TestFlight builds

4. Deploy

npm run deploy

Your server is live at https://paje.<your-subdomain>.workers.dev.

5. Build the iOS app

  1. Open ios/Paje/Paje.xcodeproj in Xcode
  2. Update the Bundle Identifier to match APP_BUNDLE_ID
  3. Select your development team
  4. Build and run on your device

On first launch, enter your server URL and subscribe to a topic.

iOS App

6. Send a notification

Post to any topic your device is subscribed to — the topic is the URL path:

curl -H "Content-Type: application/json" \
  -d '{"body":"Hello from Paje!"}' \
  https://your-server.workers.dev/my-topic

If you subscribed to my-topic in the iOS app, your phone buzzes.

Sending Notifications

Basic

curl -H "Content-Type: application/json" \
  -d '{"body":"Deploy complete"}' \
  https://your-server.workers.dev/my-topic

With title and priority

curl -H "Content-Type: application/json" \
  -d '{"title":"Build Failed","priority":"high","body":"main branch is broken"}' \
  https://your-server.workers.dev/ci

With image and click URL

curl -H "Content-Type: application/json" \
  -d '{
    "title": "New Photo",
    "url": "https://example.com/photo",
    "image": "https://picsum.photos/800/400",
    "body": "A new photo was uploaded"
  }' \
  https://your-server.workers.dev/photos

With action buttons

curl -H "Content-Type: application/json" \
  -d '{
    "title": "PR #42 Ready",
    "body": "All checks passed",
    "actions": [
      {"label":"View PR","url":"https://github.com/org/repo/pull/42"},
      {"label":"Merge","url":"https://github.com/org/repo/pull/42/merge"}
    ]
  }' \
  https://your-server.workers.dev/github

JSON Fields

Field Required Description
body Yes Notification message text
title No Notification title
priority No high, default, or low
url No URL to open when notification is tapped
image No Image URL for rich notification
actions No Array of [{"label":"...","url":"..."}] (max 3)

Include Authorization: Bearer <token> header when PUBLISH_TOKEN is set.

Authentication

By default, all endpoints are open. To require a token for publishing and subscribing:

npx wrangler secret put PUBLISH_TOKEN

Then include the token in requests:

curl -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-token" \
  -d '{"body":"Secured message"}' \
  https://your-server.workers.dev/my-topic

The token is required for:

  • Publishing messages (POST /:topic)
  • Subscribing/unsubscribing devices

In the iOS app, set the token in Settings > Access Token.

Dashboard

Dashboard

The web dashboard is enabled by default at your server URL. It shows:

  • Overview stats (subscriptions, subscribers, messages)
  • Subscription list with message counts
  • Message history per subscription
  • Send test notifications

To password-protect the dashboard:

npx wrangler secret put DASHBOARD_PASSWORD

When set, the dashboard shows a login page. API routes are unaffected.

To disable the dashboard entirely, add to wrangler.jsonc:

{
  "vars": {
    "DASHBOARD_ENABLED": "false"
  }
}

Configuration

Secrets (set via wrangler secret put)

Secret Required Description
APNS_KEY_P8 Yes Base64-encoded APNs .p8 key
APNS_KEY_ID Yes APNs key ID
APNS_TEAM_ID Yes Apple Developer Team ID
APP_BUNDLE_ID Yes iOS app bundle identifier
PUBLISH_TOKEN No When set, requires Bearer token for publish/subscribe
DASHBOARD_PASSWORD No When set, requires password to access the web dashboard
APNS_SANDBOX No Set to "true" for development/TestFlight builds

Environment variables (set in wrangler.jsonc vars)

Variable Default Description
DASHBOARD_ENABLED "true" Set to "false" to disable the web dashboard

Project Structure

worker/
├── index.ts          # HTTP routing, API endpoints
├── TopicDO.ts        # Per-subscription Durable Object (devices, messages, WebSocket)
├── RegistryDO.ts     # Global registry of subscriptions + WebSocket
├── apns.ts           # APNs JWT signing and push delivery
├── auth.ts           # Dashboard password authentication
└── env.d.ts          # Environment type definitions

src/
├── App.tsx           # React dashboard app
├── components/       # Overview, SubscriptionsList, SubscriptionDetail
├── hooks/            # Shared WebSocket hook
└── types.ts          # TypeScript types

ios/Paje/Paje/
├── Views/            # SwiftUI views
├── Models/           # SwiftData models
├── Services/         # API client, WebSocket, subscription manager
├── Theme/            # Shared styles and components
└── Extensions/       # Notification service extension (rich notifications)

License

Apache 2.0

About

Self-hosted push notifications for iOS, powered by Cloudflare Workers.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published