Skip to main content
Version: 1.0

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):

FieldTypeNotes
UserIdGuidOwning user — globally unique, so cross-tenant collisions on (UserId, DeviceId) are impossible
DeviceIdstring(500)Client-supplied stable per-install identifier (not the FCM token itself)
PlatformDevicePlatformAndroid=1, Ios=2, Web=3
Tokenstring(2000), requiredThe actual FCM registration token
LastSeenOnDateTime?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

ValuePlatform
1Android
2iOS
3Web

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:

  1. On login (and on FirebaseMessaging.instance.onTokenRefresh), call POST DeviceToken/Register with a stable deviceId (e.g. derived from a persisted UUID, not the FCM token) and the current FCM token.
  2. On logout, consider calling DELETE DeviceToken/Delete for 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):

  1. Obtain a token via the Firebase JS SDK's web push registration.
  2. Call Register the same way, with platform: 3 (Web) and a browser-persisted deviceId.
  3. 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).