Notification channels
Notification channels are fixed before the first push ships because Android channels cannot be changed after creation on a device. If you change a channel’s importance, sound, or vibration later, existing installs keep the old channel and its user settings, so the only safe move is to create a new channel. ALYT therefore keeps one small, stable taxonomy and reuses it across the platform.
Channel taxonomy
Section titled “Channel taxonomy”There are five channels, and no more. Each exists because people respond differently to the event it carries.
| Channel id | Name shown to the user | Importance | Sound | Why it is separate |
|---|---|---|---|---|
security | Security | HIGH | yes | Someone was granted or removed access; a device transfer was requested |
device_health | Device problems | DEFAULT | yes | A device went offline, or is waiting for replacement hardware |
device_activity | Device activity | LOW | no | Registered, came back online, firmware updated; recovery belongs here |
household | Household | DEFAULT | yes | An invitation, a guest grant expiring |
product | Product news | MIN | no | Release notes and announcements; off by default in the app’s own settings |
The id is the contract. A renamed id is not a rename on existing installs; it is a second channel with the old one still lingering on the phone.
Rules to follow
Section titled “Rules to follow”A recovery never uses the same channel as the failure. device.offline belongs on device_health; device.recovered belongs on device_activity. This avoids waking someone to say a problem already ended.
Quiet hours apply to the house, not the phone. A push is not a spoken announcement and is not suppressed by room quiet hours. If a notification exists only to say a device rebooted, it should use device_activity, which is silent.
Do not invent a channel per device or per property. Android shows channels as a short list a person scrolls through, so per-device channels become unusable quickly. Scope belongs in the payload, not in the channel.
iOS has no matching channel constraint, but the same five map onto interruption levels so the same taxonomy works on both platforms: timeSensitive for security, active for device_health and household, and passive for device_activity and product.
What the API exposes
Section titled “What the API exposes”GET /notifications/channels returns the taxonomy so the mobile app can create the exact set rather than a copied list that drifts over time. An app cannot be corrected later, so it should not guess.
How notifications are recorded
Section titled “How notifications are recorded”Notifications are recorded even when nothing can deliver them. The resolved channelId is stored at the moment the event is raised, by the code that knows why it happened. That avoids later string-based derivation, which is how a recovery ends up sounding like a failure.
| Written by | mqtt-bridge |
| Read by | GET /notifications?tenantId= with the standard list envelope and unreadOnly filter |
| Acknowledged by | POST /notifications/:id/read idempotently |
Recording without delivering is deliberate. A device that went offline and came back overnight is still a fact worth keeping, even if no phone buzzed.
Recipient scope
Section titled “Recipient scope”Each notification also carries clientId and propertyId, resolved when the event is raised. A device row inherits its device’s property and owning client. A row with no device, such as an invitation or an expiring guest grant, is stored with the correct scope rather than computed later.
clientId has two meanings:
clientId | Means | Who reads it |
|---|---|---|
| a client id | one household’s business | that household, and tenant staff |
NULL | tenant-wide, for product news | every household of the tenant, and staff |
NULL is the widest audience the outbox has. It is a decision, not a fallback for missing ownership.
Household membership is narrower than staff access. A notification goes to the owning household only, and expired members stop receiving it. Tenant staff read the console and can read the outbox, but they are not recipients. A tenant-wide row with clientId: null goes to every household of the tenant.
Push tokens and delivery
Section titled “Push tokens and delivery”Push delivery uses one registered token per installed app on one device. POST /notifications/push-tokens registers or re-points the current device at launch time, GET /notifications/push-tokens lists the account’s own devices for settings, and DELETE /notifications/push-tokens/:id stops pushing to one device.
The token is stored, not hashed, because it must be replayed to the push provider on every send. It is still treated as sensitive: it is never returned by any endpoint and never logged.
Registering re-points an existing token instead of inserting a second row. That prevents one handset from continuing to receive a previous owner’s notifications after sign-in changes. Deletion is scoped to the caller’s own id; another account’s id returns 404.
A provider rejection disables a token rather than deleting it. APNs 410 Unregistered and FCM UNREGISTERED set disabledAt with the reason. The list shows the token as active: false so the screen can answer “this phone stopped receiving.”
Delivery behavior
Section titled “Delivery behavior”Push delivery sweeps the outbox every 15 seconds by default. It is a sweep, not a write-path hook, so delivery never blocks ingest. A notification raised while delivery is down is picked up when the service returns.
The sweep is safe under restart because:
- each
PushDeliveryis claimed before the provider is called, guarded by the unique(notificationId, pushTokenId)pair Notification.pushCheckedAtmarks rows the sweep has already considered, even when no device exists- the batch is bounded at 50
- only 24 hours of backlog is delivered
Provider outcomes split into retryable, permanent, and the narrow case that disables a device. A successful 200 or 2xx sets sentAt. APNs 410 Unregistered and FCM UNREGISTERED or 404 are permanent and disable the token. APNs BadDeviceToken, FCM INVALID_ARGUMENT, and a malformed payload are permanent but keep the token. 429, 5xx, timeouts, and FCM 401 are retryable.
BadDeviceToken does not disable a device. That error often means a sandbox token reached production, or the reverse, which is a deployment mismatch rather than a dead install. Only the provider explicitly saying the install is gone may disable it.
Notes for developers
Section titled “Notes for developers”Keep the channel ids stable. Use the right channel for recoveries. Record the notification before delivery, and scope it at creation time. Household permissions are narrower than staff, DEV credentials act in the developer sandbox, and LIVE credentials only apply to the caller’s own installed connector.