A Django app that allows you to keep track of devices, send Expo push notifications and check their tickets for receipts. This project uses Celery and the Expo Server SDK for Python to send push messages and check push receipts in the background.
- Keep track of user device tokens and preferred languages
- Send push notifications efficiently in bulk
- Automatically retry sending messages in case of a failure
- Automatically keep track of message tickets
- Automatically check push receipts
- Automatically retry checking receipts in case of a failure
- Automatically flag inactive devices
- Django admin actions for sending messages and checking receipts
pip install django-expo-notifications
After installing the Django app, add it to your project's INSTALLED_APPS
setting:
INSTALLED_APPS = [
"expo_notifications", # <-- Add this line
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
The expo_notifications
app comes with a set of models.
To make them available in your database, run Django's migrate
management command:
python manage.py migrate
Finally, make sure your Django project is configured to use Celery. This can be done by following Celery's First steps with Django guide.
You may optionally set the following settings in your Django project's settings module. The code snippet below shows the default values for each setting.
from datetime import timedelta
EXPO_NOTIFICATIONS_TOKEN = None
EXPO_NOTIFICATIONS_RECEIPT_CHECK_DELAY = timedelta(minutes=30)
EXPO_NOTIFICATIONS_SENDING_TASK_MAX_RETRIES = 5
EXPO_NOTIFICATIONS_SENDING_TASK_RETRY_DELAY = timedelta(seconds=30)
EXPO_NOTIFICATIONS_CHECKING_TASK_MAX_RETRIES = 3
EXPO_NOTIFICATIONS_CHECKING_TASK_RETRY_DELAY = timedelta(minutes=1)
At some point, you most likely want to enable Enhanced Security for Push Notifications
in your Expo account.
This will require you to provide an Expo access token in your Django project's settings module like this:
EXPO_NOTIFICATIONS_TOKEN = "your-expo-viewer-access-token-here"
Check out the Expo documentation for more information.
Expo recommends checking the receipts of push notifications after some delay. This gives Expo time to process the push notifications and generate receipts. The default delay is based on the value used in the official Expo Server SDK for Node. Feel free to adjust this setting to your needs:
EXPO_NOTIFICATIONS_RECEIPT_CHECK_DELAY = timedelta(hours=1)
However, note that Expo only keeps ticket receipts for around a day and Celery generally prefers if tasks are not scheduled too far in the future.
The most basic usage of this app involves managing user devices and sending messages to them. Everything else (i.e. background task scheduling, retries, checking ticket receipts, deactivating devices, etc.) is handled automatically.
from django.contrib.auth import get_user_model
from expo_notifications.models import Device, Message
some_user = get_user_model().objects.first()
device = Device.objects.create(
user=some_user,
push_token="ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
)
device.messages.send(
title="Hello, World!",
body="This is a test message.",
)
The Device
model represents a user device that can receive push notifications as long as it's is_active
flag is True
.
A user may have multiple devices, however, each device must have an Expo token that is unique to that device and user.
from django.contrib.auth import get_user_model
from expo_notifications.models import Device
some_user = get_user_model().objects.first()
Device.objects.update_or_create(
user=some_user,
push_token="ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
defaults={"is_active": True},
)
We recommend using update_or_create
to avoid duplicate devices and to automatically reactivate existing devices if necessary.
The Device
model has an optional lang
field that can be used to store the language/region code of the device.g. as per ISO 639-1).
This can be useful if you want to send localized push notifications to your users.
from django.contrib.auth import get_user_model
from expo_notifications.models import Device
some_user = get_user_model().objects.first()
Device.objects.update_or_create(
user=some_user,
push_token="ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
defaults={"is_active": True, "lang": "en"},
)
We recommend using ISO 639-1 and if needed, ISO 3166-1 alpha-2 codes.
The recommended way to send messages is to create and send them in bulk like this:
from expo_notifications.models import Device, Message
messages = [
Message(device=device, title="Hello, World!")
for device in Device.objects.active
]
Message.objects.bulk_send(messages)
Similar to bulk_send
, we provide a send
method that can be used to send a single message:
from expo_notifications.models import Device, Message
device = Device.objects.active.first()
Message.objects.send(device=device, title="Hello, World!")
Both bulk_send
and send
methods use Django's bulk_create
and create
under the hood and are also available on related managers:
from expo_notifications.models import Device
device = Device.objects.active.first()
device.messages.send(title="Hello, World!")
While creating and sending messages in one go is the recommended way, you can also create messages first and send them later:
from expo_notifications.models import Message
# Sending a single message (i.e. a model instance)
single_message = Message.objects.first()
single_message.send()
# Sending multiple messages (i.e. a queryset)
multiple_messages = Message.objects.all()
multiple_messages.send()
The expo_notifications
app comes with Django admin actions that can be used to send messages and check their receipts.
Note that these actions are mainly meant for troubleshooting and manual testing.
The Send selected messages
action can be used to troubleshoot your setup and test sending push notifications.
The action will send the selected messages in bulk via a Celery task and schedule a task to check their receipts.
To see whether the messages were sent successfully, you can check their tickets in the Django admin interface.
Messages which were rejected by Expo will have their is_success
flag set to False
and their error_message
field may contain a rejection reason.
The Check selected tickets
action can be used to manually check the receipts of selected messages without a delay.
This action is solely meant for troubleshooting, since the app automatically schedules tasks to check the receipts of all sent messages.
To see whether a message was successfully sent to a device, you can check its ticket receipts in the Django admin interface.
Messages which could not be sent to a device will have their is_success
flag set to False
and their error_message
field may contain a reason.
Take a look at our Django example project under tests/project
.
It can be run by executing these commands:
uv sync
uv run tests/project/manage.py migrate
uv run tests/project/manage.py createsuperuser
uv run tests/project/manage.py runserver
Sending push notifications and checking their receipts can be done by using the "Send selected messages" action in the Django admin interface. In order for this to work, you need to run a Celery worker and make sure the Celery worker is authorized to communicate with Expo.
Unless the default Celery broker setup works for you (i.e. RabbitMQ on localhost), you need to provide a Celery broker url:
export CELERY_BROKER_URL="redis://localhost:6379/0"
In case you enabled Enhanced Security for Push Notifications
in your Expo account, you also need to provide an Expo access token.
export EXPO_NOTIFICATIONS_TOKEN="your-expo-viewer-access-token-here"
Now you can run a Celery worker and the Django dev server in parallel to test sending push notifications and checking their receipts:
uv run celery --workdir tests/project --app project worker -l INFO
uv run tests/project/manage.py runserver