Skip to content
Browse documentation
Decided

Notifications

Every notification the app produces, how it is delivered (in-app, push, e-mail), its wording, and what the user can control.

Updated: 29 October 2026

Kinds

KindTriggerPush title / body (EN)Tap opens
invitation_receivedsend_invitation“New invitation” / “Anna wants to connect with you”3.1.3 Received invitation
invitation_acceptedaccept_invitation“Anna accepted” / “You’re now connected”contact card
contact_updatedsnapshot hash changed for a connection whose circle has notifications on“Anna updated her details” / “Phone number and address” (changed categories)contact card, changed rows highlighted
circle_changedother party moved you to a circle and your view changed“Anna changed what you can see” / (no body)contact card
systemoperator message (rare: policy update requiring acceptance, forced update)text from the rowin-app dialog

No notification when a connection ends (A102, 2026-10-29). Removing a contact, blocking, deleting a managed profile and deleting an account used to tell the other side with a contact_removed notification. For privacy they now tell nobody: the card leaves the other person’s list at their next refresh, and their connected services put the entry in the delete bucket as before. The kind is gone from the table above, from notifications.kind, from push and from Settings.

Dutch strings are in the i18n tables; the server picks the language from devices.locale. Bodies never contain the detail values themselves.

Delivery

  1. A trigger or function inserts a notifications row.
  2. A database webhook calls push-fanout, which sends one message per row to every device of the account. Coalescing happens where the row is written, not here: while an unread contact_updated or circle_changed for the same connection already exists, insert_notification drops the new one. A timed batching window on top of that is deferred until there is traffic to measure.
  3. The app, when in the foreground, receives the row through Realtime and updates the bell badge without showing a system banner (Saymail’s rule: no banner for what the user is already looking at).
  4. In the background the push arrives. Android receives a data-only message (kind, id) and builds the notification itself in the “Contacts” channel, in the device’s language, then refreshes; iOS receives one alert carrying content-available: 1, so the app refreshes before the notification is tapped, with thread-id grouping the kind and badge set to the account’s unread count.
  5. There is no second, silent push: the one message on each platform both shows the notification and wakes the app.

Controls

Settings → Notifications:

  • Allow notifications: the OS permission state, read again every time the screen comes back to the front. Three states: on; not asked yet (Android 13 and later, or refused once, when the system dialog may still be shown), with Allow, which shows it and registers this device for push the moment it is granted (A59); and off (refused for good, or switched off in system settings), with Open settings, which opens the app’s own notification page (Settings.ACTION_APP_NOTIFICATION_SETTINGS). Android cannot say whether it has ever asked, so the app remembers on the device that Settings asked; that flag is a fact about the phone and survives sign-out.
  • Per kind switches: Invitations (default on, covers invitation_received and invitation_accepted), Contact updates (default on), Circle changes (default off). They are account columns (notify_invitations, notify_contact_updates, notify_circle_changes, A83; notify_contact_removed is dropped with the kind, A102), so they hold for every device of the account, including one that is off. push-fanout reads them and skips the push for a kind that is off; the notification row is still written, so the in-app list shows every kind, which the screen says under the switches. system has no switch and is always pushed. Each switch is written to the account at once and put back with an error line if the server refuses.
  • Per circle: managed from the circle’s own switch “Send me updates about people in this circle”.
  • Quiet hours are left to the OS (Focus modes / Do Not Disturb); the app does not implement its own.
  • E-mail digests: none at launch. Only transactional e-mails exist (sign-in codes, verification codes, e-mail change codes, deletion and export confirmations), each in the account’s language (A92).

In-app list (3.1.5)

Needs you first (round 4, variant 2; mock mobile/design/redesign/redesign-round-5.html). Two sections:

  • Needs you: received invitations not yet answered and not ignored, each row “Anna wants to connect”, “2 days ago · says she is your sister”, with Answer and Ignore (Contacts, A103).
  • Updates, with “2 new” on the right: everything else, newest first. A contact update says what changed and what happened to it: “Bram · new mobile number”, “Today 09:12 · written to Google” or “Yesterday · waits for sync” (from the connected services’ state for that connection); “Joris accepted”, “Yesterday · in Friends”; “Anna changed what you can see”.

Each avatar carries a small badge for the kind (phone, address, person-add, check). Unread rows are bold with a dot; swipe to mark read; “Mark all read” in the top bar. An ignored invitation does not count towards the bell or the app icon badge. Notifications older than 90 days are pruned server-side. Tapping a notification about a profile that no longer exists shows “This person is no longer on Incico”.

Android specifics

One notification channel “Contacts” (importance default) plus a silent “Sync” channel for the foreground-less background sync status if ever needed. No foreground service, no boot receiver; FCM is the only wake-up (Saymail’s decision).

iOS specifics

UNUserNotificationCenter delegate publishes the tapped notification’s id through the app; the app handles it after the store has loaded. Badge count is set from the unread count on every refresh. Provisional authorisation is not used; the permission is asked in onboarding step 2.4 after an explanation.