Self-hosted push notification and Live Activity infrastructure for apps that want to own their users, tokens, jobs, and delivery state while still using APNS and FCM as the final device transports.
Pushboy started as a Go learning project and grew into a focused notification service: user and topic token storage, APNS and FCM dispatch, scheduled fanout, delivery receipts, and a shared worker pool that handles both regular pushes and Live Activity updates.
- Why Pushboy - the ownership problem this repo solves
- What It Does - APNS, FCM, topics, scheduling, receipts, and Live Activities
- Project Status - current release scope
- Quick Start - run the service locally
- Setup Guide - detailed Docker, Postgres, APNS, and FCM setup
- Docker - build and run the container
- Configuration - environment variables
- API Examples - common curl flows
- Architecture - shared worker pool and storage boundaries
- Comparison - Firebase, OneSignal, AWS SNS, and Gorush
- OpenAPI - machine-readable API spec
- Documentation - supporting project docs
- License - MIT
APNS and FCM deliver to devices, but most apps still need a backend layer that owns application users, device tokens, topics, scheduled jobs, receipts, and Live Activity state. Managed products can solve that, but they usually move the ownership boundary into a hosted platform. Pushboy keeps that orchestration layer in your infrastructure while still using APNS and FCM as the final transports.
- Sends visible, silent, rich, and scheduled push notifications through APNS and FCM.
- Tracks users, device tokens, topics, subscriptions, publish jobs, counters, and delivery receipts.
- Supports user-scoped and topic-scoped fanout.
- Supports APNS and FCM Live Activity flows through the same job pipeline.
- Auto-subscribes new users to a configurable broadcast topic.
- Uses Postgres as the first-class storage backend today, with a
Storeinterface for adding other databases. - Exposes a compact HTTP API with an OpenAPI 3.1 spec in docs/openapi.yaml.
- Runs as a single binary or Docker container.
v0.0.0 is the first OSS preview release. It packages the core HTTP service, Postgres-backed storage, APNS/FCM dispatch, Live Activity jobs, Docker setup, OpenAPI docs, and setup scripts. The roadmap is tracked in GitHub milestones for CI, tests, auth, metrics, durable queue adapters, targeting, templates, imports, and dashboard work.
Requirements:
- Go 1.24+
- Postgres
- APNS
.p8credentials and/or Firebase service-account JSON if you want real sends
Fast Docker setup:
curl -fsSL https://raw.githubusercontent.com/mithileshchellappan/pushboy/main/scripts/setup.sh | sh
cd ~/pushboy
docker compose up --buildTo pin a specific release:
curl -fsSL https://raw.githubusercontent.com/mithileshchellappan/pushboy/main/scripts/setup.sh | PUSHBOY_VERSION=v0.0.0 shManual local setup:
git clone https://github.com/mithileshchellappan/pushboy.git
cd pushboy
cp .env.example .envCreate a Postgres database and update DATABASE_URL in .env:
createdb pushboyRun the server:
go run ./cmd/pushboyThe app runs Postgres migrations from db/migrations/postgres during startup. Check the process:
curl http://localhost:8080/v1/pingExpected response:
pong
Build the image:
docker build -t pushboy:dev .Or run Pushboy and Postgres together:
docker compose up --buildRun it with a reachable Postgres URL and mounted provider credentials:
docker run --rm \
-p 8080:8080 \
--env-file .env \
-v "$PWD/keys:/app/keys:ro" \
pushboy:devInside Docker, localhost points at the container itself. If Postgres is running on your host, use a Docker-accessible hostname such as host.docker.internal on macOS, or put Pushboy and Postgres on the same Docker network.
The image runs as a non-root user, exposes port 8080, copies Postgres migrations into /app/db/migrations, and includes a liveness health check against /v1/ping.
| Variable | Default | Notes |
|---|---|---|
SERVER_PORT |
:8080 |
HTTP bind address. Use a private network or gateway in production. |
DATABASE_URL |
postgres://localhost:5432/pushboy?sslmode=disable |
Postgres connection string. |
WORKER_COUNT |
10 |
Master workers that fan out jobs into token batches. |
SENDER_COUNT |
200 |
Sender workers that call APNS/FCM. |
JOB_QUEUE_SIZE |
1000 |
Buffer size for in-process queues. |
BATCH_SIZE |
5000 |
Token batch size read from Postgres. |
MAX_RETRY_NOTIFICATION |
3 |
Number of notification retry attempts. |
APNS_KEY_ID |
empty | Apple Developer key id. Enables APNS when present and readable. |
APNS_TEAM_ID |
empty | Apple Developer team id. |
APNS_BUNDLE_ID |
APNS_TOPIC_ID fallback |
iOS bundle id. Live Activities use <bundle>.push-type.liveactivity. |
APNS_KEY_PATH |
derived from key id | Path to the APNS .p8 file. |
APNS_USE_SANDBOX |
false |
Set true for sandbox APNS. |
FCM_KEY_PATH |
keys/service-account.json |
Firebase service-account JSON. project_id is read from this file. |
BROADCAST_TOPIC_NAME |
broadcast |
New users are subscribed to this topic when configured. |
Create a topic
curl -X POST http://localhost:8080/v1/topics/ \
-H "Content-Type: application/json" \
-d '{"id":"broadcast","name":"Broadcast"}'Register an APNS or FCM device token
curl -X POST http://localhost:8080/v1/users/tokens \
-H "Content-Type: application/json" \
-d '{
"id": "user-123",
"platform": "apns",
"token": "device-token"
}'Send a notification to one user
curl -X POST http://localhost:8080/v1/users/user-123/send \
-H "Content-Type: application/json" \
-d '{
"title": "Order update",
"body": "Your driver is nearby.",
"collapse_id": "order-123",
"data": {
"orderId": "order-123"
}
}'Publish to a topic
curl -X POST http://localhost:8080/v1/topics/broadcast/publish \
-H "Content-Type: application/json" \
-d '{
"title": "Maintenance complete",
"body": "All systems are back online."
}'Schedule a future notification
curl -X POST http://localhost:8080/v1/topics/broadcast/publish \
-H "Content-Type: application/json" \
-d '{
"title": "Reminder",
"body": "Your session starts soon.",
"scheduled_at": "2026-05-01T18:00:00Z"
}'Register a Live Activity token
curl -X POST http://localhost:8080/v1/live-activity/tokens \
-H "Content-Type: application/json" \
-d '{
"userId": "user-123",
"topicId": "orders",
"activityId": "order-123",
"platform": "apns",
"tokenType": "update",
"token": "live-activity-update-token"
}'Start a Live Activity
curl -X POST http://localhost:8080/v1/live-activity/jobs \
-H "Content-Type: application/json" \
-d '{
"action": "start",
"activityId": "order-123",
"activityType": "OrderDeliveryAttributes",
"userId": "user-123",
"payload": {
"status": "driver_assigned",
"etaMinutes": 18
},
"options": {
"alert": {
"title": "Order update",
"body": "Your driver is on the way."
},
"attributesType": "OrderDeliveryAttributes",
"attributes": {
"orderId": "order-123"
},
"priority": "high"
}
}'flowchart LR
Client["App backend or trusted API caller"] --> API["Pushboy HTTP API"]
API --> Service["Service layer"]
Service --> Store["Store interface (Postgres)"]
Service --> JobPool["shared job pool"]
Scheduler["Scheduled job sweeper"] --> JobPool
JobPool --> Masters["master workers"]
Masters --> Store
Masters --> TaskPool["shared task pool"]
TaskPool --> Senders["sender workers"]
Senders --> APNS["APNS"]
Senders --> FCM["FCM HTTP v1"]
Senders --> OutcomePool["shared outcome pool"]
OutcomePool --> Outcomes["outcome worker"]
Outcomes --> Store
The shared pool is the important design choice. Push jobs and Live Activity dispatches enter the same job pipeline, then branch by JobType. Master workers page tokens from Postgres, sender workers call the platform transport, and the outcome worker writes receipts and counters back to Postgres.
The Pipeline[T] and Store boundaries are intentionally small. Postgres has first-class support today, and another database can be added by implementing the Store interface. The in-process channel pipeline is the default queue today; Redis, Kafka, and other queue backends can be added behind the same pipeline boundary.
APNS and FCM are still the device transports; Pushboy is the self-hosted orchestration layer above them.
| Capability | Pushboy | Firebase Cloud Messaging | OneSignal | AWS SNS | Gorush |
|---|---|---|---|---|---|
| Source model | MIT open source | Proprietary Google-managed service | Proprietary hosted service | Proprietary AWS-managed service | MIT open source |
| Deployment | Self-hosted Go binary/container | Google-managed | OneSignal-managed | AWS-managed | Self-hosted Go binary/container |
| Primary shape | Push and Live Activity orchestration | Device push transport | Engagement platform | Pub/sub and mobile push service | Push gateway |
| APNS support | Yes | Yes, through FCM setup | Yes | Yes | Yes |
| FCM support | Yes | Native | Yes | Yes | Yes |
| Extra push providers | Adding support soon | No | Web, Huawei, Amazon, macOS, Windows | Other AWS-supported endpoint types | HMS |
| Topic fanout | App-owned topic table | FCM topics and conditions | Audiences/segments/tags | SNS topics with mobile endpoints | No persisted app topic model |
| User-token-topic ownership | Built in | You build it | Platform-owned | You build app user mapping on top | You supply tokens per request |
| Persisted jobs and receipts | Built in | Provider message ids and Firebase tooling | Platform analytics | CloudWatch/SNS delivery status options | Stats/metrics focus |
| Live Activities | Yes, with unified activity sends | Yes | Yes | Yes | No |
| SDK dependency | None required for server callers | Client SDK and Admin SDK are the normal path; HTTP v1 also exists | SDK-centered for identity, delivery tracking, and Live Activities; REST API for sends | AWS SDK/API centered | REST API and CLI |
Pushboy owns the application layer above APNS and FCM: users, device tokens, app topics, jobs, receipts, and unified Live Activity dispatch state.
Public docs checked for this comparison: FCM Live Activities, OneSignal Live Activities, AWS SNS mobile push, and Gorush.
The API spec lives at docs/openapi.yaml.
- Setup guide
- OpenAPI spec
- Postman collection
- Security policy
- Contributing guide
- Code of conduct
- Changelog
- Postgres migrations
- Systemd example
MIT License. See LICENSE.