Skip to content

Notifications — Providers

Overview

The notifications.providers package contains integrations with external push notification systems.

It provides a clean abstraction layer between:

  • internal notification logic (services, tasks)
  • external delivery providers (Expo, FCM, APNS, etc.)

This layer ensures that:

  • providers are interchangeable
  • delivery logic is consistent
  • external APIs are isolated

Responsibilities

The providers layer is responsible for:

  • sending push notifications to external services
  • translating provider responses into a unified format
  • handling provider-specific payload formats
  • exposing receipt APIs (when applicable)

It is not responsible for:

  • retry logic
  • scheduling
  • delivery state management
  • notification creation
  • preference handling

Core Abstractions

PushSender (interface)

Defines the contract for sending push notifications.

Method:

  • send_batch(provider, tokens, title, body, data)

Responsibilities:

  • accept a batch of tokens
  • return per-token results
  • remain provider-agnostic

PushSendResult

Represents the result of sending to a single device.

Fields:

  • ok: whether the provider accepted the message
  • provider_message_id: provider-specific ID (e.g. Expo ticket)
  • error: error message (if failed)
  • invalid_token: whether token should be deactivated

This unified shape allows the rest of the system to remain provider-independent.


DefaultPushSender

The default implementation of PushSender.

Responsibilities:

  • route requests to the correct provider implementation
  • support multiple providers
  • return per-token results even for unsupported providers

Current behavior:

  • routes "expo" to Expo implementation
  • returns failure results for unsupported providers

This acts as a dispatcher layer.


Expo Provider

send_push_to_tokens

Main entrypoint for sending Expo push notifications.

Responsibilities:

  • chunk tokens into safe batch sizes
  • send batches to Expo API
  • merge results into a single mapping

Chunking

Expo limits the number of messages per request.

The provider:

  • splits tokens into chunks (typically 100)
  • sends each chunk separately
  • aggregates results

HTTP sending

The provider supports two modes:

Real HTTP mode

  • sends requests to Expo endpoints
  • parses response payload
  • returns per-token results

Stub mode

  • returns synthetic success results
  • used for development and testing

Controlled via settings:

  • NOTIFICATIONS_EXPO_USE_HTTP

Error handling

The provider distinguishes:

Retryable errors

  • HTTP 429 (rate limiting)
  • HTTP 5xx (server errors)
  • network failures

These raise exceptions so the task layer can retry.

Non-retryable errors

  • HTTP 4xx
  • per-token errors in response

These return PushSendResult(ok=False, error=...).


Receipt handling

Expo push is asynchronous.

Flow:

  1. Push send returns a ticket ID
  2. Ticket ID is stored (ExpoPushTicket)
  3. Later, receipts API is called
  4. Final delivery status is resolved

expo_get_receipts

Fetches delivery receipts from Expo.

Responsibilities:

  • query receipt API
  • return receipt data per ticket ID
  • raise errors for retryable failures

ExpoReceiptsClient

Abstraction for receipt fetching.

Allows:

  • injecting mock clients in tests
  • decoupling HTTP layer from task logic

push_base utilities

chunk_list

Generic helper for splitting lists into chunks.

Used by:

  • Expo provider
  • potentially other providers

Provider flow

flowchart TD
    A[Task: process_pending_deliveries] --> B[PushSender]
    B --> C[DefaultPushSender]
    C --> D{Provider}

    D -->|expo| E[Expo send_push_to_tokens]
    E --> F[Chunk tokens]
    F --> G[Send HTTP requests]
    G --> H[Parse responses]
    H --> I[PushSendResult per token]

    D -->|unsupported| J[Return failure results]

Receipt flow

flowchart TD
    A[Task: check_expo_push_receipts] --> B[ExpoReceiptsClient]
    B --> C[Expo receipts API]
    C --> D[Receipt data]
    D --> E[Update ExpoPushTicket]
    E --> F[Deactivate invalid devices]


Design principles

Provider isolation

  • all external API logic lives in providers
  • no provider code in services or views

Unified result shape

  • all providers return PushSendResult
  • simplifies downstream logic

Batch-first design

  • providers operate on batches
  • improves performance and reduces API calls

Fail-fast for retryable errors

  • raise exceptions for retryable failures
  • allow task layer to handle retries centrally

Testability

  • injectable sender/client interfaces
  • stub mode for development
  • no hard dependency on external APIs in tests

What providers do not do

Providers do not:

  • schedule retries
  • persist delivery state
  • decide recipients
  • apply preferences
  • trigger notifications

Those responsibilities belong to:

  • services
  • tasks
  • models

Best practices

  • always use PushSender abstraction (never call provider directly)
  • keep provider implementations stateless
  • return results for every token
  • raise only for retryable batch-level failures
  • isolate HTTP logic inside provider modules
  • use stub mode in non-production environments

Extensibility

The providers layer is designed to support:

  • additional push providers (FCM, APNS)
  • email providers
  • SMS providers
  • multi-provider fallback strategies
  • provider-specific optimizations

To add a new provider:

  1. implement PushSender behavior
  2. map provider string in DefaultPushSender
  3. ensure PushSendResult compatibility

Summary

The notifications.providers package isolates all external delivery integrations.

It ensures:

  • consistent delivery behavior
  • provider independence
  • testability
  • scalability across channels

This allows the rest of the system to remain clean and focused on business logic.