Notifications
What is the notification system
A unified dispatch layer that turns device-originated events (shadow updates and direct notify/… topics) into outbound messages on one or more delivery channels — mobile push, webhooks (Alexa, GVA, …), and automation triggers. Devices declare what happened and which channels should react; the dispatcher routes from there.
Why it is needed
Devices should not know about APNS, GCM, OAuth bearer tokens, or which user has which app installed. They publish a shadow update or a direct notification on the notify/… topic; the cloud decides who needs to be told and how to talk to them.
This also lets us add new delivery channels (e.g. SMS, email, a new voice assistant) without touching device firmware or the dispatcher — a new channel is just a new entry in the service registry.
Pre-requisites
Node is registered and bound to a group/subgroup.
User has access to that group/subgroup (see group.md).
For user-specific channels: the user has registered with the relevant channel (see notifications-push.md, notifications-webhooks.md).
Architecture
Device --(MQTT)--> IoT Core ----> Notifications Lambda
|
| build dispatch event
| validate node-group
| resolve user list (group fanout)
|
v
Service registry
/ | | | \
push alexa gva automation ...
| | |
SNS Publish HTTP POST + OAuth
Topic naming
Both the notify topic and the named shadow are built from the node’s membership (group + all its subgroups, sorted, from getGroupInfo) and verified against it by step 3 of the Dispatch flow. Read each row left-to-right: membership decides the string, and the string decides the fanout.
Node membership |
Notify topic (direct notification) |
Named shadow (shadow update) |
Fanout recipients |
|---|---|---|---|
group + subgroup(s) |
|
|
group-level users + those subgroups’ users |
group only (no subgroup) |
|
|
group-level users only as no subgroups |
Rules:
The notification kind is decided by the source: the
notify/topic segment marks a direct notification (the group string is the segment afternotify/, with no prefix); theparams-shadow-name prefix marks a shadow update.Group ID must be non-empty (it is the first hyphen-separated token of the group string).
The publishing node must actually belong to the named group — enforced by the dispatcher (see Dispatch flow, step 3).
Notification kinds
The dispatcher builds a channel-agnostic event from the incoming topic:
Shadow update
Triggered by a delta on a params-… shadow.
Carries:
node ID
shadow name (full topic name)
delta — diff between previous and current reported state
current reported state
Direct notification
Triggered by a publish on a rainmaker/nodes/<nodeID>/notify/… topic.
Carries:
node ID
the device-supplied notify payload
In both cases, group and subgroup IDs are populated from the topic/shadow name and travel with the event.
Event payload
The dispatcher Lambda receives an event of the form:
{
"node_id": "abcd1234",
"topic_name": "params-g1abc-s01",
"notification_type": "shadow_update",
"curr_state": { "params": { ... }, "data": { ... }, "online": true },
"prev_state": { "params": { ... } },
"notify": {
"version": "1.0",
"push": { ... },
"alexa": { ... },
"gva": { ... },
"automation": { ... }
}
}
Field |
Meaning |
|---|---|
|
Required for |
|
|
|
Reserved — skipped by the dispatcher. |
|
Map of channel → channel-specific config. Each non- |
Unknown service keys are logged and skipped, never failed.
Service registry
Each channel registered in the registry is one of two kinds:
Generic — invoked once per dispatch. Used for channels that don’t fan out to users (e.g.
automation).User-specific — invoked once per user, after the dispatcher resolves the user list for the originating group/subgroup.
Service |
Kind |
Notes |
|---|---|---|
|
user-specific |
Alexa Smart Home ChangeReport. See alexa.md. |
|
generic |
Internal automation triggers. |
|
user-specific |
Google HomeGraph Report State. See gva.md. |
|
user-specific |
Mobile push via SNS. See notifications-push.md. |
|
generic |
Server-side action, not a delivery channel: a node reporting a self factory-reset is disassociated and its data cleaned up. Valid only as a direct notification. |
|
generic |
Registered only when |
Each service supplies its own marshal step that converts the channel-agnostic event into a channel-specific payload (a push message, a ChangeReport, a HomeGraph request, …). See the per-channel specs for those shapes.
Dispatch flow
The notifications Lambda runs once per incoming event:
Build the channel-agnostic event from the topic name and state. Reject events with unknown
notification_typeor missing required state.Parse group and subgroup IDs from the topic/shadow name.
Validate node-group alignment — fetch the node’s actual group/subgroup membership and confirm it matches what the topic name claims. If it does not, drop the event silently. This is the security gate against a device fabricating a topic that targets a group it does not belong to.
Iterate over the keys of
notify(skippingversion):Look up the service in the registry. Unknown service → log and continue.
Run the channel’s marshal step. Failure → log and continue.
Generic service → invoke once with the marshalled payload.
User-specific service → resolve the user list for the originating group/subgroup, then invoke the service per-user. Empty user list → log and continue.
Errors from any individual service are logged but do not abort the loop — one failing channel must not block the others.
Group fanout semantics
The user list comes from the same access-control model as the rest of the platform (see group.md). Two classes of user exist in a group:
Group-level users (primary/secondary owners, not scoped to a subgroup) are always in the recipient list, for every event in the group.
Subgroup-scoped users are included only when the publishing node is in a subgroup they are scoped to (the node’s subgroup set intersects theirs).
A node’s recipient set is fixed by its membership — there is no per-node choice or fallback:
Node in one or more subgroups → group-level owners plus the users of those subgroups.
Node in no subgroup → group-level owners only.
Per-channel specs
notifications-push.md — mobile push (APNS, GCM) via SNS Mobile Push.
notifications-webhooks.md — outbound webhooks with OAuth (Alexa, GVA, custom).
Automation is registered as a service for completeness, but its mechanics are an internal feature, not an outbound delivery channel.
Endpoint identity per integration
Every delivery channel a user has registered is one endpoint. An endpoint is the smallest addressable delivery target — what PUT /v1/integrations/{integrationId}/endpoints creates and what the response’s endpoint_id names. The natural identifier differs by integration type, and so does what “multi” means (i.e. when the same user can have more than one row for the same integration).
Integration |
What “multi” means |
endpoint_id |
|---|---|---|
Alexa |
Multiple Amazon accounts the same user has linked (rare — most users link once) |
Amazon user ID from LWA |
APNS / GCM |
Multiple devices the same user installs the app on |
SNS Endpoint ARN (per device token) |
GVA |
Multiple Google accounts the same user has linked (also rare) |
The |
Webhook |
Multiple webhook subscriptions per user |
The |
Multiple integrations per type
The same question one layer up: can an admin register more than one integration of the same integration type? Differs by type, because the integration’s identity and config store differ.
Integration type |
Multiple per deployment? |
Why |
|---|---|---|
Alexa |
No — singleton |
Config lives at fixed SSM paths ( |
APNS / APNS_SANDBOX |
Yes — one per |
|
GCM |
Yes — one per Firebase |
|
GVA |
No — singleton |
Same shape: fixed SSM path ( |
Webhook |
No — fixed set at deploy time |
Channels are compiled into the service registry; there is no admin API to add one (see notifications-webhooks.md). A generic webhook-registration API would give each registered webhook its own integration_id and make this row “yes”. |