Django push notifications to the browser
Searches for “Django push notifications” often end in Firebase tutorials. You don’t need Firebase for browsers: standard Web Push with VAPID works in Chrome, Firefox, Edge and Safari. Here’s a compact Django implementation, and the packages that save time.
- Model: endpoint (unique), p256dh, auth, optional user.
- Serve the service worker from a root URL like /sw.js so its scope covers the site.
- Send with pywebpush in a Celery or RQ task; delete subscriptions on 404/410.
- Packages: django-webpush (simple helpers) or django-push-notifications (web plus native).
1. A subscription model
# push/models.py
from django.conf import settings
from django.db import models
class PushSubscription(models.Model):
endpoint = models.URLField(max_length=1000, unique=True)
p256dh = models.CharField(max_length=200)
auth = models.CharField(max_length=100)
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
def info(self):
return {'endpoint': self.endpoint, 'keys': {'p256dh': self.p256dh, 'auth': self.auth}}2. Subscribe view and a root service worker
# push/views.py
import json
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .models import PushSubscription
@require_POST
def subscribe(request):
data = json.loads(request.body)
PushSubscription.objects.update_or_create(endpoint=data['endpoint'], defaults={
'p256dh': data['keys']['p256dh'], 'auth': data['keys']['auth'],
'user': request.user if request.user.is_authenticated else None})
return JsonResponse({'ok': True}, status=201)
# urls.py
from django.urls import path
from django.views.generic import TemplateView
from push.views import subscribe
urlpatterns = [
path('push/subscribe', subscribe),
path('sw.js', TemplateView.as_view(template_name='sw.js', content_type='application/javascript')),
]Serving the worker from Django at /sw.js gives it scope /. Static files live under /static/, and a worker there could only control /static/. Send the CSRF token from the browser with an X-CSRFToken header (read from the csrftoken cookie), as Django’s docs describe for AJAX.
// sw.js — show the notification and open the link on click
self.addEventListener('push', e => {
const d = e.data ? e.data.json() : {};
e.waitUntil(self.registration.showNotification(d.title || 'Update', { body: d.body, icon: '/icon-192.png', data: { url: d.url || '/' } }));
});
self.addEventListener('notificationclick', e => {
e.notification.close();
e.waitUntil(clients.openWindow(e.notification.data.url));
});3. Send in a background task
# push/tasks.py (Celery shown; RQ or Django-Q work the same way)
import json
from celery import shared_task
from django.conf import settings
from pywebpush import webpush, WebPushException
from .models import PushSubscription
@shared_task
def notify_user(user_id, title, body, url='/'):
for sub in PushSubscription.objects.filter(user_id=user_id):
try:
webpush(sub.info(), json.dumps({'title': title, 'body': body, 'url': url}),
vapid_private_key=settings.VAPID_PRIVATE_KEY,
vapid_claims={'sub': 'mailto:' + settings.VAPID_ADMIN_EMAIL}, ttl=86400)
except WebPushException as e:
if e.response is not None and e.response.status_code in (404, 410):
sub.delete()Push services answer 201 when they accept a message. 404 or 410 means the subscription is gone — delete it. 413 means the payload is too large (keep it under about 4 KB), 429 means slow down. Details in the Web Push protocol.
Packages that save time
| Package | What it gives you | Notes |
|---|---|---|
| django-webpush | Model, subscribe view, template tags for a button, send_user_notification() / send_group_notification() | Configure WEBPUSH_SETTINGS with your VAPID keys and admin email |
| django-push-notifications | Device models for APNs, FCM, WNS and Web Push | Useful if you also have native apps |
| pywebpush (direct) | Just the sending | Most control; you write the model and views (as above) |
Check each project’s repository for current Django version support before adopting it.
Production tips
- Keep VAPID keys in environment variables or a secrets manager, loaded in settings.
- Use
select_related/batching when sending to thousands; chunk into tasks of a few hundred. - Upsert the subscription on login so it’s attached to the user.
- Respect quiet hours with the subscriber’s timezone if you store it.
- Add a “Turn off notifications” control that calls
subscription.unsubscribe()and deletes the row.
Send a test from the Django admin
# push/admin.py
from django.contrib import admin
from .models import PushSubscription
from .tasks import send_one
@admin.action(description='Send test notification')
def send_test(modeladmin, request, queryset):
for sub in queryset:
send_one.delay(sub.id, 'Test', 'If you can read this, push works.')
@admin.register(PushSubscription)
class PushSubscriptionAdmin(admin.ModelAdmin):
list_display = ('endpoint', 'user', 'created')
actions = [send_test]A one-click test from the admin saves a lot of time when support asks “are notifications working for this user?”.
Async views and ASGI
pywebpush is synchronous. In async views, don’t call it directly — enqueue a task or use sync_to_async with a thread-sensitive setting appropriate for your database use. The subscribe view itself is cheap and can stay synchronous.
When not to build it
Everything above is the do-it-yourself route: you own the keys, storage, sending queue and cleanup. If what you need is marketing-style notifications to anonymous visitors in your Django app — a branded prompt, segments, schedules, A/B tests, automations, iPhone Home Screen handling — Pusharoo does that for free: add the snippet to your base template and upload sw.js. You can still call Pusharoo’s REST API from Django for transactional sends (POST /api/v1/notify) and receive signed webhooks when people subscribe, so the two approaches mix well.
Questions
Does Django have built-in push notifications?
No. Use pywebpush directly or a package like django-webpush or django-push-notifications.
Do I need Firebase for Django web push?
No. Firebase is only needed if you choose FCM; standard VAPID push reaches Chrome, Edge, Firefox and Safari.
Can I send to iPhone from Django?
Yes, to users who added your site to the Home Screen on iOS 16.4+ and subscribed there; the same Web Push code works.
How do I serve the service worker from Django’s static files?
Static files live under /static/, which limits the worker’s scope. Either serve it via a view at /sw.js (as shown) or have your web server map /sw.js to the static file and add a Service-Worker-Allowed: / header.
Can I send push from a Django signal?
Yes, but enqueue a task inside the signal handler rather than sending synchronously, so saving a model never waits on push services.
Keep reading
Web push in Python with pywebpush
pywebpush end to end: py-vapid keys, Flask subscription endpoint, sending with TTL, deleting 404/410 subscriptions.
Service worker push events, done right
push and notificationclick handlers, focusing open tabs, action buttons, subscription changes and pitfalls.
VAPID keys for web push, explained
What the key pair does, how the signed JWT works, how to generate keys and why rotation breaks lists.
The Web Push API in JavaScript: a working example
Register a worker, ask on click, subscribe with VAPID, store the subscription and send — complete code.
More in Developers: The Web Push protocol, explained · Web push notifications with Node.js · Web push notifications in PHP · Laravel browser push notifications (no Firebase) · Web push notifications in React and Next.js · Declarative Web Push, explained · Firebase web push vs. standard web push · Web push notifications in Ruby on Rails · Web push in Go with webpush-go · Angular push notifications with SwPush · Push notifications in Vue 3 and Nuxt · Push notifications in SvelteKit · All guides
Free for life. No limits.
Unlimited subscribers, campaigns and automations. No badges. About five minutes to install.