Notifications
XMB notifies people about what happens on their servers — a request being approved, a season arriving, a hub going offline — through four channels: the in-app inbox, push (native apps and browsers), email, and Discord.
Three services carry the surface, all under xmb.api.v1:
| Service | What it covers |
|---|---|
NotificationService | The inbox, unread counts, the alert board, the preference grid, and admin broadcasts |
NotificationPushService | Registering a device for push |
NotificationDiscordService | Tying a server to a Discord guild, and linking your own Discord account |
Events and alerts
Notifications come in two shapes that never share a list:
- An event happened at a moment. It lands in an inbox, is read or unread, and ages out after 30 days. "Your request was declined" stays true forever.
- An alert is a condition that is true right now — a hub offline,
storage low, a tracker account failing, playback errors clustering. It
carries a severity (
info,warn,crit), clears itself when the condition ends, and never becomes history. Current kinds:hub_offline(crit),grabber_offline,storage_low,tracker_auth_failed,playback_failing,task_failing(warn),review_backlog,update_available(info, board-only — never announced).
Players participate in one of these: a player whose pipeline genuinely
errors should call StopPlayback with failure_code (short machine code)
and failure_detail (the player's own words). A clean stop leaves both
empty. Clustered failures on one server raise the playback_failing
alert.
Events are read with ListNotifications, which serves two separate feeds
per server: the viewer audience (your requests, series you watch,
announcements) and the admin audience (requests to approve, sourcing
trouble, maintenance). The admin feed requires administering the server.
Alerts are read with ListAlerts, admin-only, most severe first.
GetUnreadCounts is the cheap poll behind the bell: viewer unread, admin
unread, and the count and loudest severity of active alerts, in one call.
Clients typically poll it every 30 seconds. MarkNotificationsRead marks
specific notifications (by uuid) or everything in one audience.
Unknown NotificationKind values must render gracefully: every notification
carries its own title, body and target_path, so a client never needs
per-kind logic — kinds added after your client shipped still display.
Preferences
GetNotificationPreferences returns the caller's grid: rows are categories
(my-requests, series, new-in-library, membership, announcements,
and the admin-only to-approve, acquisition, server-health,
maintenance), columns are channels (in_app, push, email,
discord). Each cell is ENABLED, DISABLED, or UNAVAILABLE —
unavailable is a fact about the category (the weekly digest never pushes),
renders dimmed, and updates against it are rejected.
Preferences are account-scoped: no server slug, one grid that follows
you across every server you belong to. UpdateNotificationPreferences
takes only the cells being changed and returns the whole resolved grid.
One exception is written into the product: a critical server-health alert (a hub going offline) always reaches admins by push and email, whatever the grid says.
Broadcasts
SendBroadcast (admin) sends a free-form announcement — title, plain-text
body, optional in-app link — to every member of a server through their own
channel preferences. Setting urgent forces push delivery regardless of
preferences; use it sparingly. The response reports how many members it
fanned out to.
Push devices
Push registration is account-scoped: a token is a fact about your device and fires for every server you are on.
- Call
GetPushConfigto learn what the deployment supports — APNs (iOS), FCM (Android) and Web Push availability, plus the VAPID public key a browser needs to subscribe. - Obtain a token: the raw device token from the OS push machinery in the
native apps (
getDevicePushTokenAsync()— an APNs device token on iOS, an FCM registration token on Android), orPushManager.subscribe()in a browser (serialise the subscription JSON). - Call
RegisterPushDevicewith a stable per-installinstall_id, the token kind (APNS,FCMorWEBPUSH), and the token. Registration is an upsert — call it again freely; on every app start with permission granted is correct, and how token rotation is survived.
UnregisterPushDevice withdraws an install's token on sign-out. Tokens the
push services report dead are retired automatically.
Don't prompt for notification permission on app load — ask when the person first enables a push preference.
Discord
One platform Discord application serves every server, in two independent
ways. When a deployment has no Discord application configured, both
surfaces report NOT_CONFIGURED and clients should render nothing.
Tying a server to a guild (admin)
BeginDiscordGuildConnectreturns Discord's add-to-server consent URL; the admin picks the guild in Discord's own UI (Discord enforces that they manage it) and is redirected back.ListDiscordChannelslists the guild's text channels the bot can actually post to, for the channel pickers.UpdateDiscordChannelsroutes the two audiences: the admin channel carries the admin feed, the members channel carries server-wide member news (new in library, series drops, announcements). Personal notifications never post to a shared channel.SendDiscordTestposts a labelled test message to a routed channel and surfaces Discord's exact error when it fails — this is the permission validator to point admins at.
GetDiscordIntegration reports status (including BOT_REMOVED when the
bot was kicked and CHANNEL_MISSING when routing points at a deleted
channel), and DisconnectDiscordGuild severs the tie.
Linking your own account (anyone)
BeginDiscordAccountLink returns a consent URL for the identify scope;
completing it links your Discord identity, after which the discord column
of your preference grid delivers by direct message from the bot. The OAuth
grant is used once to read your identity and discarded.
GetDiscordLink reports the link, including DM_BLOCKED — the bot cannot
message you, usually because your privacy settings block DMs from server
members or you share no guild with the bot. DisconnectDiscordAccount
forgets the identity.
Messages posted to Discord neutralise mentions and markdown in quoted content, and never include emails, addresses or file paths.