Skip to content
Developers · 9 min read

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.

Updated By the Pusharoo team
In short
  • 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

PackageWhat it gives youNotes
django-webpushModel, 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-notificationsDevice models for APNs, FCM, WNS and Web PushUseful if you also have native apps
pywebpush (direct)Just the sendingMost 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.

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.