11. Device Tokens
Device tokens are how the framework knows which Firebase Cloud Messaging (FCM) tokens belong to which user, across however many devices/browsers they're signed into.
11.1 Entity
DeviceToken (table DeviceTokens, framework-owned, namespace-shimmed into Shumoul.Domain.Entities.Notifications):
| Field | Type | Notes |
|---|---|---|
UserId | Guid | Owning user — globally unique, so cross-tenant collisions on (UserId, DeviceId) are impossible |
DeviceId | string(500) | Client-supplied stable per-install identifier (not the FCM token itself) |
Platform | DevicePlatform | Android=1, Ios=2, Web=3 |
Token | string(2000), required | The actual FCM registration token |
LastSeenOn | DateTime? | Updated on every register/refresh — used to order GetUserDevices results |
11.2 Registration flow — upsert, not insert
DeviceTokenService.RegisterAsync (backing POST Register):
The lookup uses IgnoreQueryFilters() specifically to find soft-deleted rows too — this is safe only
because UserId is a globally unique GUID (a cross-tenant match is not possible), and is a narrow, justified
exception to the platform's general "never .IgnoreQueryFilters() in tenant-facing services" rule
(.claude/rules/multitenancy.md) — not a precedent for other services.
Why upsert matters: FCM tokens rotate (app reinstall, OS-level refresh, browser cache clear). Without
upsert-by-(UserId, DeviceId), every token refresh would either violate a unique constraint or accumulate
stale duplicate rows that the Push channel would keep sending to (and failing against).
11.3 Platform values
| Value | Platform |
|---|---|
1 | Android |
2 | iOS |
3 | Web |
11.4 Consumption at dispatch time
IDeviceTokenService.GetActiveTokensForUserAsync(userId) returns every Token where
UserId == userId && IsActive && Is_Deleted != true, with no platform filtering — the Push channel provider
sends to every active token for a user regardless of platform, since FCM handles platform-specific delivery
transparently once a valid token is supplied.
11.5 Client integration checklist
Flutter:
- On login (and on
FirebaseMessaging.instance.onTokenRefresh), callPOST DeviceToken/Registerwith a stabledeviceId(e.g. derived from a persisted UUID, not the FCM token) and the current FCMtoken. - On logout, consider calling
DELETE DeviceToken/Deletefor the current device so a shared/kiosk device stops receiving another user's push notifications — the framework does not do this automatically on logout today.
Angular (web push):
- Obtain a token via the Firebase JS SDK's web push registration.
- Call
Registerthe same way, withplatform: 3(Web) and a browser-persisteddeviceId. - Re-register whenever the SDK reports a token change event.
11.6 Operator view
GET GetUserDevices/{userId} (§9.3) lists a
user's active devices, most-recently-seen first — useful for support/troubleshooting ("did this user's phone
ever register for push?") without exposing the raw FCM token grid to non-admin roles (that requires
DeviceTokens.ViewAll via GetDataTable instead).