How to use bitcaster-django
Configuration
Add bitcaster_django to INSTALLED_APPS and configure the app with the
BITCASTER dictionary in your settings:
INSTALLED_APPS = [
...
"bitcaster_django",
]
BITCASTER = {
# Bitcaster Application Endpoint.
# Falls back to the BITCASTER_BAE environment variable when empty/omitted.
"BAE": "https://<token>@<host>/api/o/<organization>/",
# forwarded to the sdk client (optional, default: False)
"DEBUG": False,
# keep Bitcaster users aligned with Django users (optional, default: True)
"SYNC_USERS": True,
# project/application slugs used by Client.trigger_event() and the user
# synchronisation (required when SYNC_USERS is enabled).
# Fall back to the BITCASTER_PROJECT_SLUG / BITCASTER_APPLICATION
# environment variables when empty/omitted.
"PROJECT": "myprj",
"APPLICATION": "myapp",
# distribution list synced users are subscribed to (optional).
# Falls back to the BITCASTER_DISTRIBUTION_LIST environment variable.
"DISTRIBUTION_LIST": "mylist",
# fully qualified name of the sdk client class to use
# (optional, default: "bitcaster_sdk.client.Client")
"CLIENT": "bitcaster_sdk.async_client.AsyncClient",
}
All keys except BAE, SYNC_USERS, PROJECT, APPLICATION,
DISTRIBUTION_LIST and CLIENT are forwarded (lowercased) to the
bitcaster-sdk client
constructor, so any bitcaster-sdk option can be configured from the dictionary.
CLIENT selects the sdk client implementation: the default
bitcaster_sdk.client.Client blocks on every call, while
bitcaster_sdk.async_client.AsyncClient runs requests on a background thread
and returns concurrent.futures.Future objects. Any other fully qualified
name of a bitcaster_sdk.abstract_client.AbstractClient subclass works too.
The configuration is validated by the Django system check framework:
python manage.py check
Usage
Triggering events
Map a local event name to a remote Bitcaster event slug with the EventConfig
model (e.g. from the Django admin or a data migration):
from bitcaster_django.models import EventConfig
EventConfig.objects.create(local_name="signup", remote_event_slug="user-signup")
then trigger the event by its local name through the Client facade:
from bitcaster_django.client import Client
Client().trigger_event("signup", context={"username": user.username})
The event is triggered on the configured PROJECT/APPLICATION. Keeping the
mapping in the database decouples the names used in your code from the slugs
defined on the Bitcaster server, so they can be changed without redeploying.
Managing users
The Client facade also wraps the sdk user management API. Application-level
membership (on the configured PROJECT/APPLICATION):
from bitcaster_django.client import Client
client = Client()
client.register_user("username", "First", "Last", "user@example.com", active=True)
client.unregister_user("username")
and organization-level management:
client.add_user("user@example.com", "First", "Last")
client.update_user("user@example.com", "First", "Last")
client.delete_user("user@example.com")
Advanced clients
The bitcaster_django.advanced module provides drop-in subclasses of the
sdk clients — Client (sync) and AsyncClient — extended with a django
namespace of Django-aware helpers. Opt in via the CLIENT setting:
BITCASTER = {
...
"CLIENT": "bitcaster_django.advanced.Client",
# or "bitcaster_django.advanced.AsyncClient"
}
The django namespace triggers events for a subset of the recipients, on
top of the sdk trigger_event API (the recipient filter travels in its
options parameter, any caller-provided options are preserved):
from bitcaster_django.client import Client
sdk = Client().sdk # the configured advanced client
sdk.set_domain("myprj", "myapp")
# only the users with the given usernames
sdk.django.trigger_for_users("user-signup", ["u1", "u2"], context={"k": "v"})
# only the users belonging to at least one of the given Django groups
sdk.django.trigger_for_groups("user-signup", [managers_group.pk])
trigger_for_groups resolves the group members to usernames using the
local Django database (the source of truth for the memberships synced to
Bitcaster — see the groups custom field above); the query always runs in
the calling thread, even with the async client. Both methods raise
ValueError on an empty recipient list (an empty filter could otherwise
reach every recipient), and return what the underlying client returns:
plain data for the sync client, concurrent.futures.Future objects for the
async one.
Using the sdk directly
The bitcaster_sdk client is initialized automatically when Django starts, so
the bitcaster-sdk API can also
be used directly anywhere in your code — no bitcaster_sdk.init() call needed:
import bitcaster_sdk
bitcaster_sdk.ping()
bitcaster_sdk.list_users()
User synchronisation
When SYNC_USERS is enabled (the default), post_save/post_delete handlers
on your AUTH_USER_MODEL keep Bitcaster aligned with your Django users for
their whole lifecycle, using the username (get_username()) as the lookup
key. PROJECT and APPLICATION must be configured (enforced by the system
checks).
- Saving a Django user registers it as member of the configured
PROJECT/APPLICATION, creating the Bitcaster user when missing and updating the membership otherwise — the same idempotent call covers creation, update, deactivation and reactivation. - The membership
activeflag mirrors the user'sis_active, so deactivating a Django user suspends it in Bitcaster without losing any Bitcaster-side data; user models without anis_activeattribute are treated as always active. - When the user has an email address, it is registered as an address assigned
to the preferred channels, and the resulting assignments are subscribed to
the
DISTRIBUTION_LISTdistribution list (when configured). Users without an email are registered without addresses. - Deleting a Django user unregisters it, deleting its application membership records.
Targeting Django groups with Bitcaster filters
The membership custom fields always carry the pks of the Django groups the
user is assigned to, as a dict[str, list[int]]:
{"groups": [3, 8, 11]}
The mapping is kept up to date as users are added to or removed from groups
(from either side of the relation: user.groups.add(...) or
group.user_set.add(...)). Since Bitcaster filter payloads can match the
membership custom fields, this lets a notification target only the users
belonging to a certain Django group — e.g. filtering on groups containing
the pk of your managers group sends the message to the Django managers
only, without maintaining a dedicated distribution list.
Any error while talking to Bitcaster is logged (logger
bitcaster_django.handlers, including the username, to allow manual
reconciliation) but never breaks the saving or deletion of the Django user;
unregistering a user unknown to Bitcaster is not an error.
Known limitations:
- bulk operations (
QuerySet.update()/QuerySet.delete()) do not emit per-instance signals, so they are not synced; - the sdk only uses names and email when creating the Bitcaster user: renaming a user or changing its email does not propagate to an already existing Bitcaster user;
- unregistering a user does not remove its distribution-list subscriptions;
- clearing a group from its reverse side (
group.user_set.clear()) provides no affected-user information, so thegroupscustom field of those users is not refreshed until their next save or group change.
Runtime configuration with django-constance
Install the optional extra:
pip install bitcaster-django[constance]
then add constance to INSTALLED_APPS and declare the BITCASTER keys you
want to manage at runtime as BITCASTER_<KEY> constance keys:
INSTALLED_APPS = [
...
"constance",
"bitcaster_django",
]
CONSTANCE_CONFIG = {
"BITCASTER_BAE": ("", "Bitcaster Application Endpoint"),
}
Configuration resolution order:
- constance
BITCASTER_<KEY>key (when declared and non-empty) - the
BITCASTERsettings dictionary - built-in defaults
- the environment variables, when the resolved value is empty
(
BITCASTER_BAE,BITCASTER_PROJECT_SLUG,BITCASTER_APPLICATIONandBITCASTER_DISTRIBUTION_LISTforBAE,PROJECT,APPLICATIONandDISTRIBUTION_LISTrespectively)
The sdk client is reinitialized automatically whenever a BITCASTER_*
constance key is updated (config_updated signal), so the endpoint can be
changed from the admin without restarting the application. At startup the
client is initialized from the static settings; any value stored in constance
is picked up on the first request.