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¶
See Push notifications (mobile).
Related¶
- Mobile contract: guides/push_notifications.md
- Live play WS: guides/play_client.md
- Users (notify prefs): users.md