Skip to content

Push Notifications (admin)

Database configuration for remote push notifications. Path: /admin/push-notifications (Advanced nav).

Mobile contract and full decision order: guides/push_notifications.md.

Rate limits & constraints

Shown at the top of the admin page in General, Issuance, and Resolve groups (values from lib.push.constants — not editable in the form):

General

Constraint Typical value
Expo project ceiling 600/s
Our global Redis send budget 500/sec across Celery workers
Expo HTTP batch size ≤100 messages/request
Receipt ticket Redis TTL 3600s (pending tickets for Beat poll)
Title / body soft caps 100 / 200 chars; {{question_text}} truncated at 120

Also: one Celery task per event; Expo MessageRateExceeded / APNs·FCM / Focus can still block delivery.

Ticket vs receipt: a successful send only means Expo issued a ticket (ticket_ok). Beat’s poll_expo_push_receipts (every minute) fetches Expo receipts, upgrades attempts to receipt_ok / receipt_error, and disables devices on DeviceNotRegistered. Details: guides/push_notifications.md § Receipts & hygiene.

Issuance

Constraint Typical value
Audience Followers with eligible device + notify_on_issuance
Freshness In-game: abandon if age >10s or remaining <10s. Match questions: abandon if age >10s or already locked. Re-checked per chunk
Delivery TTL In-game: remaining answer window, capped at 60s. Match questions: seconds until scheduled start, capped at 900s

Resolve

Constraint Typical value
Audience Followers who answered + notify_on_resolve (not retract/stale)
Freshness Abandon if resolved >180s ago (re-checked per chunk)
Delivery TTL 60s

Tuning & message copy

Stored in stored_settings key push_notification_settings. Saves apply to the next Celery send (no worker restart).

Field Default Meaning
Notify on issuance on Master switch for in-game and Match question issuance pushes
Notify on resolve off Resolve pushes to answerers only (in-game and Match questions; not retract/stale)
Pivotal only on In-game issuance pushes only for pivotal moments. Does not suppress Match questions
Min seconds between device 45 Per-device cooldown (shared by in-game and Match questions)
Max issuances per user per match 20 Issuance cap (0 = unlimited; separate Redis counter; shared by in-game and Match questions)
Max resolves per user per match 20 Resolve cap (0 = unlimited; separate Redis counter; shared by in-game and Match questions)
Attempt retention days 7 Prune window for attempt rows
Title / body templates see defaults Title and issuance bodies use {{match_label}}, {{match_shortcode}}, {{question_text}}. Resolve body is not on this page — Expo composes League Defaults default_resolve_copy with stamped resolve_copy and outcome_text.

Reset to defaults restores code defaults.

Secrets (EXPO_ACCESS_TOKEN) are process env / ECS only — not on this form.

Global vs user preferences

Layer Fields Notes
Global (this page) notify_on_issuance, notify_on_resolve Master kill switches for the whole product
Per user User.notify_on_issuance, User.notify_on_resolve Default on; edit in Users or PATCH /api/users/me

A user cannot receive resolve pushes until both the global resolve gate and their personal pref are on (and they followed + answered).

Per-user delivery lookup

Enter an exact username, exact email, or user UUID. Shows:

  • Registered devices (platform, disabled, updated, last success) with browser-local times
  • Recent push_delivery_attempts (outcome, skip / error, match shortcode, shortened ids with copy, ticket, rendered title/body when the send stored them)

Times use the operator browser timezone. Filter by event (all / issuance / resolve) and attempt day (all / today / yesterday / last 5 days / custom local days). The attempts table is sortable and paginated (10 / 25 / 50 / 100; default 50). The table scrolls vertically and horizontally; Time and Event stay frozen. Title/body stay on one line (no wrap or truncation). Skip / error has a ? listing every skip_reason and common provider_error values.

provider_error stores transport triage text (HTTP status + validation message, ticket code + message, or receipt error) — not only the exception class name. Title/body appear only on rows that actually attempted a send (not skips).

Rows older than retention are pruned; use Celery logs for older history.

push_devices.token is unique; duplicate push tokens from regenerated client device_ids are reclaimed on register (and cleaned before the unique index migration).

Useful skip reasons: match_push_disabled, user_notify_on_*_false, disabled, cooldown, duplicate_token, match_cap, pivotal_only, stale_issuance / stale_resolve, global notify_on_*_false. Missing devices are not logged as attempts — check the user’s device list. Full table in the mobile guide.

Test send script

uv run python scripts/push/send_test_push.py --email player@example.com

See Push notifications (mobile).