# django-push-notifications: four device models, four push services, one settings dict

> JazzBand's app wraps APNS, FCM, WNS and WebPush behind four near-identical Django models. The design is dull in the best way, and the edge cases are where it shows.

**jazzband/django-push-notifications** — Send push notifications to mobile devices through GCM or APNS in Django.

- Repository: https://github.com/jazzband/django-push-notifications
- Stars: 2,385 · Forks: 636
- Language: Python
- License: MIT
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/jazzband-django-push-notifications

## Four models that differ only in how the message is delivered

The package is described as a minimal Django app implementing Device models that can send messages through APNS, FCM/GCM, WNS and WebPush. It implements four models, `GCMDevice`, `APNSDevice`, `WNSDevice` and `WebPushDevice`, and the design decision that makes it worth reading is that all four share exactly the same attributes.

That list is short and worth reading closely. `name` is an optional label. `active` defaults to True and decides whether the device gets messages at all, which is how you disable a device without deleting it. `user` is an optional foreign key to your user model, so a device only has to be linked to a person if your app needs per-user targeting. `device_id` is an optional UUID obtained from the platform SDK. And `registration_id` is the only required field, holding the FCM registration id, the APNS token, or the equivalent for that service.

Because the shape is identical, the admin panel can offer the same two actions for every device type: send a test message, or send a test message in bulk. The README is careful about what bulk means, noting that a non-bulk test to more than one device simply iterates and sends single messages. That clarification is the kind of thing a README usually omits.

## Installation with extras, one per transport

Dependencies are declared per backend rather than all at once, so you install only what you send through.

```shell
pip install django-push-notifications[WP,apns-async,FCM]
```

The extras map to `pywebpush` 1.3.0 or newer for WebPush, `py-vapid` 1.3.0 or newer for generating the WebPush private key, `apns2` 0.3 or newer for synchronous Apple Push, `aioapns` 3.1 or newer for the async Apple path, and `firebase-admin` 6.2 or newer for FCM. Django REST Framework 3.7 or newer is required only if you use the API module.

The aioapns and apns2 relationship is the one detail that will bite you. The README states plainly that installing aioapns overrides apns2, and gives the reason: apns2 does not support Python 3.10 or later. So the two are alternatives rather than complements, and picking both is not an error the package will warn you about at install time.

The stated baseline is Python 3.7 and Django 2.2, which is worth holding against the release history rather than trusting outright. Native Django migrations are in use, so `manage.py migrate` installs and migrates all models with no extra step.

## Settings live in one dict, and the APNS half is the fussy half

Everything is configured through a single `PUSH_NOTIFICATIONS_SETTINGS` dict, which is the pattern that keeps a four-backend package from becoming four configuration systems.

The general settings are few. `USER_MODEL` names your user model and defaults to `settings.AUTH_USER_MODEL`. `UNIQUE_REG_ID` forces `registration_id` to be unique across device models, with a caveat: the README links a current MySQL bug ticket and the Django text field limitations page, because MySQL cannot use that setting as intended. `UPDATE_ON_DUPLICATE_REG_ID` is the more interesting one, transforming a create on an already-registered device into an update, which is the behaviour you want in production where a client may re-register at any time.

APNS is where the detail piles up, and this is the section to read twice. `APNS_CERTIFICATE` takes an absolute path to a certificate file and cannot handle one with a passphrase. There is a separate path for token-based authentication: `APNS_AUTH_KEY_PATH` for a `.p8` signing key, plus `APNS_AUTH_KEY_ID` and `APNS_TEAM_ID`, both 10-character identifiers from your Apple developer account. The README says to use the key path instead of the certificate if you have a `.p8` file.

Then there is the sandbox trap. `APNS_USE_SANDBOX` switches the host to the development endpoint, and the default depends on your `DEBUG` setting. The README warns that if you set `APNS_USE_SANDBOX=True` you must supply the development certificate, not the production one, or the app will connect to the wrong host. There is also `APNS_USE_ALTERNATIVE_PORT` for 2197, and `APNS_TOPIC`, which is usually your bundle id and falls back to the certificate Subject if omitted.

## WNS and WebPush settings, and why the key step is off-server

WNS, the Windows Notification Service, needs two values, and here the README contradicts itself about one of them. The sample settings dict uses `WNS_PACKAGE_SECURITY_ID`, shown as an `ms-app://` URI alongside a placeholder `WNS_SECRET_KEY`, while the prose a few paragraphs earlier tells you that you need `WNS_PACKAGE_SECURITY_KEY` and `WNS_SECRET_KEY`. Both facts sit in the same file. If you are configuring WNS, the workable test is to use the name from the sample dict and check whether WNS sends succeed, because the documentation itself will not settle it and the failure mode is a silent no-op rather than an error.

WebPush takes `WP_PRIVATE_KEY` as a path to your key file and `WP_CLAIMS`, whose sample value is a `sub` claim containing a mailto address. That claim is not decoration: it is the contact address a push service uses to report a permanently failed subscription, so leaving it out means you never hear about devices that should be cleaned up.

The README also makes a point about where the WebPush key gets generated, stating that py-vapid is needed for generating the private key but that this step does not need to occur on the application server. That is a security recommendation rather than a technical requirement, and it is a good one: the key is a long-lived credential and keeping it out of the app tier reduces the blast radius of a compromised server.

For FCM, the setup happens in your own settings file rather than in the package dict. The sample shows calling `firebase_admin.initialize_app()` at import time and reading credentials from the environment, with a note that options such as an HTTP timeout can be passed through. The migration path from the legacy FCM APIs to HTTP v1 is documented separately in `docs/FCM.rst`.

## Release history shows a package chasing platform drift

The recent releases describe a project spending its effort on compatibility, which is the honest shape of a package like this one.

Version 3.2.1 in March 2025 is four small Apple-side features: support for `mutable_content` and for `category` in aioapns, and support for `content_available` for APNS async, plus a change making `mutable-content` an integer 1 rather than a boolean. That last one is the kind of detail that only exists because a platform library's type annotation disagreed with reality.

Version 3.2.0 in January 2025 fixes the default FCM max recipients, modernizes the distribution builds, fixes error handling on duplicated APNS devices, and adds a database index on `registration_id` for the APNS device model. That last pair is a nice illustration of the ongoing cost of this design: because registration ids are looked up and de-duplicated on every send path, the field needs an index, and someone had to notice.

Version 3.3.0 in November 2025 adds Python 3.12 and 3.13 support, pins `aioapns` below 4.0 to prevent breaking changes, adds an explicit Django AppConfig to set `default_auto_field`, fixes webpush timeouts and the webpush API, and then reverts a typing annotation pull request in the same release. The revert is the most informative entry, since it suggests the typing work was larger than the release could carry. `pyproject.toml` shows the pytest configuration wiring up branch coverage reporting, and ruff configured with tab indentation.

There is no homepage listed in the repository metadata. The project has 2,385 stars, 636 forks and 127 open issues, MIT licensed, with the last push on 2026-09-22.

## Conclusion

The core of this package is a good idea executed without ambition: one device model shape, replicated four times, with the transport-specific code confined to a sending layer. That means a project supporting both iOS and Android does not maintain two device registries, and the admin panel test actions work identically across all four. The rough edges are concentrated in the certificate and sandbox settings, which follow Apple's older conventions more than its current token-based authentication docs, and in a dependency list where the async APNS client deliberately overrides the sync one. Read the settings list in the README before choosing extras, since a wrong sandbox or certificate combination fails silently at connection time.

## FAQ

### How do I install django-push-notifications with only the backend I need?

Use extras. `WP` pulls pywebpush and py-vapid for WebPush, `apns-async` pulls aioapns for async Apple Push, and `FCM` pulls firebase-admin. The sync Apple backend is apns2, and installing aioapns overrides it because apns2 does not support Python 3.10 or newer, so treat the two as alternatives.

### What is the difference between APNS_CERTIFICATE and APNS_AUTH_KEY_PATH?

The certificate setting takes an absolute path to an APNS certificate file and does not support one protected by a passphrase. The key path setting is for token-based authentication with a `.p8` signing key, and it needs the matching 10-character key ID and team ID alongside it. Use the key path when you have a `.p8` file.

### Why do my APNS notifications fail when DEBUG is on?

The default for `APNS_USE_SANDBOX` depends on your `DEBUG` setting, which switches the host to the development endpoint. If you force sandbox mode you must supply the development certificate rather than the production one, because a mismatch sends the app to the wrong host. The README calls this out explicitly as a setup step people miss.

### How do I stop duplicate devices being created for the same phone?

Set `UPDATE_ON_DUPLICATE_REG_ID`, which turns a create on an already-registered device into an update. For a hard uniqueness constraint you can set `UNIQUE_REG_ID`, though the README notes a MySQL bug prevents that setting from working as intended there. The `active` flag is the softer option: it keeps the row but stops messages being sent.

### Does django-push-notifications support WebPush for browsers?

Yes, through a `WebPushDevice` model alongside the APNS, FCM and WNS ones. It needs pywebpush 1.3.0 or newer and py-vapid for generating the private key, and the README recommends generating that key outside the application server so a long-lived credential is not exposed there.

## Sources

- [Issues](https://github.com/jazzband/django-push-notifications/issues)
- [jazzband/django-push-notifications on GitHub](https://github.com/jazzband/django-push-notifications)
- [License: MIT](https://github.com/jazzband/django-push-notifications/blob/master/LICENSE)
- [README](https://github.com/jazzband/django-push-notifications/blob/master/README.md)
- [Releases](https://github.com/jazzband/django-push-notifications/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jazzband-django-push-notifications
