Notifications
The activity feed that pulls you back
Overview
Reverse-chronological list of notifications, grouped by 'today / this week / older'. Unread items have a subtle olive dot. Each row has an actor avatar, the verb ('reacted to your goal', 'wants to be your friend'), and a relative timestamp. Tapping accepts/declines inline for friend requests, routes to the activity for reactions, or to the vault for mineral changes.
How it works
`notifications.list` returns paginated rows, hydrated with the actor's user doc. Tapping a row calls `notifications.markRead` which sets `readAt` on the row.
Push notifications are delivered via Expo's push service. The agent's `expoPushTokens` array stores up to two tokens (one per device); each notification fan-outs to all tokens.
On notification tap from outside the app, the payload includes a `route` and `params` field which the root `DeepLinkHandler` parses and routes to.
Server-side, every write that creates a notification also enqueues a push send via Convex action. The action is fire-and-forget; failures are logged but don't break the originating write.
Token registration happens on app launch via `useNotificationToken`, which runs `Notifications.getExpoPushTokenAsync()` and writes the result to `users.registerPushToken`.
Key decisions
Push as a Convex action, not the mutation
Sending a push from inside the mutation that wrote the notification would couple write success to network success. We isolate sends in a Convex action that the mutation enqueues — the write commits regardless of whether the push lands.
Inline accept/decline for friend requests
Routing every friend request to a dedicated screen adds friction. The notification row itself has Accept and Decline buttons that fire the corresponding mutation and animate the row out. The friendship is established without a screen change.
