PushNotificationServerFramework is an open-source template for building remote push notification servers for iOS applications using the Apple Push Notification service. It handles device registration and delivers notifications through APNs.
- Device registration: Idempotent registration keyed by device token, with optional device metadata.
- Push delivery: Per-token results; tokens Apple reports as gone are pruned automatically.
- APNs client: Persistent HTTP/2 connection, token (.p8) or certificate authentication, current APNs push types and error reasons.
- API key authentication: Bearer-token protection on push and admin endpoints.
- Data persistence: SQLAlchemy 2.0 with PostgreSQL.
- Tested: Unit, API, and Postgres-backed integration suites run in CI with lint and type checks.
apis/: API endpoints.auth.py: API key dependency.database.py: Database engine and session dependency.entities/: SQLAlchemy entities.models/: Pydantic request and response models.push/: APNs client and push handling.services/: Application services.tests/: Unit, API, and integration tests.utils/: Environment helpers.
- Python 3.11+
- PostgreSQL (any reachable instance; a disposable Docker one is shown below)
git clone https://github.com/j0shcap/PushNotificationServerFramework.git
cd PushNotificationServerFramework
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.template .env # then fill in your values
docker run -d --name pnsf-postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:16-alpine
python main.pyInteractive API documentation is served at http://127.0.0.1:8000/docs.
python main.py binds to 127.0.0.1:8000; set HOST and PORT to override. Behind a reverse proxy, run uvicorn directly with workers:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2Terminate TLS at the proxy — the API key travels in a header and must never cross plain HTTP.
Configure the application through the .env file. Database and APNs identifiers are required; the notable options:
API_KEY(required): the secret protected endpoints require. The server refuses to start without it.APNS_CERT_PATH: switches from token auth (the default, recommended by Apple) to certificate auth. Points to a PEM file containing the provider certificate and private key;APNS_CERT_PASSWORDsupplies its passphrase if any.APNS_USE_SANDBOX: set totruewhen testing with development builds; their device tokens are only valid against the APNs sandbox environment.CORS_ORIGINS: comma-separated origins allowed to make cross-origin requests. Unset by default, which disables CORS entirely — iOS apps do not use CORS; only set this when serving a web frontend.DB_ECHO: set totrueto log SQL statements during development. Off by default because statements include device tokens.
Endpoints that send pushes or expose device data require the API key:
Authorization: Bearer <API_KEY>
Requests without a valid key receive 401 Unauthorized. /devices/register is deliberately open: it is called by the iOS app itself, and shipping the key inside the app binary would expose it. The worst an unauthenticated caller can do is register junk tokens, which APNs pruning removes on the next push.
To implement push notifications in an iOS application:
- Register the application for push notifications (Apple Developer documentation).
- Request permission from the user to send push notifications.
- Post the device token to the
/devices/registerendpoint. APNs hands the app the token as rawData; convert it to the hex string this server expects:func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02x", $0) }.joined() // POST ["token": token] to /devices/register }
- Endpoint:
/devices/register - Method:
POST - Body:
Also accepts the optional device fields listed under Design Notes. Server-managed fields (
{ "token": "hex_apns_device_token" }id, timestamps) are ignored if sent. - Response: The registered device, including its server-assigned
idand timestamps. Registration is idempotent by token: re-registering updates the stored fields.
- Endpoint:
/devices/all - Method:
GET - Auth: Requires API key
- Endpoint:
/devices - Method:
DELETE - Auth: Requires API key
- Endpoint:
/push/send - Method:
POST - Auth: Requires API key
- Body:
{ "recipients": ["device_token_1", "device_token_2"], "body": "notification_body" } - Response: A mapping of each device token to
"Success"or the APNs failure reason. A failure for one token does not prevent delivery to the others.Devices whose tokens APNs reports as{ "device_token_1": "Success", "device_token_2": "Unregistered" }Unregisteredare automatically removed from the database.
The Device entity and its model represent a device registered with the server. This device includes the device token and optionally additional device information available in the UIDevice class.
token: The device token used to send push notifications to the device. (Required, String)name: The name of the device. (Optional, String)systemName: The name of the operating system running on the device. (Optional, String)systemVersion: The current version of the operating system. (Optional, String)model: The model of the device. (Optional, String)localizedModel: The model of the device as a localized string. (Optional, String)
Cross-origin requests are disabled by default. To develop a web frontend against the server, set CORS_ORIGINS to the exact origins you serve it from (never a wildcard in production).
Note: CORS is a browser security feature that prevents cross-origin requests. It does not affect requests from iOS applications.
Run the unit and API test suite:
pytestIntegration tests boot the real server against a real Postgres and drive it over HTTP. Point them at any Postgres instance (for example, a disposable container):
docker run -d --name pnsf-test-pg -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:16-alpine
INTEGRATION_DB_HOST=localhost pytest tests/integrationCI runs both suites, plus ruff and mypy, on every push and pull request.
Contributions to this repository are welcome. Please follow the standard GitHub pull request process to propose changes.
This project is licensed under the MIT License.
- Pydantic for data validation and serialization.
- SQLAlchemy ORM for database management.
- FastAPI for the server framework.
- The Python
apns2package, from which the vendored APNs client is derived.