People
Notifications
What people are told, in the app, about what others do: who is told what, who never is, how a notification goes when what it tells of goes, and how they are delivered. The plan they follow is plans/done/notifications.md (opens in a new tab) (roadmap rows 96 and 97).
No email
Notifications are never emailed. They are in the app only, under the bell. The only emails Anahita sends are auth-service's, each about one thing that happened: codes, password resets and security alerts; invitations; signup requests and answers; a group or an account scheduled for deletion.
A test keeps it so: anahita-libs/data/services/mail_senders_test.go fails
if any service but auth-service and mail-grpc-service uses the mail client.
What a notification is
A row in notifications, one for each person told, written by
notification-service when it hears an event on the bus (anahita_events).
| Column | Holds |
|---|---|
type |
What it tells of: the kinds below |
recipient_id |
Who is told |
subject_id |
Who did it |
object_id |
What it was done to or with: a post, for most kinds |
target_id |
Where: the profile it belongs to |
comment_id |
What holds what was done, when that is not the object: a reply, or a repost |
read_at |
When the recipient read it: NULL while it is unread |
Nothing is copied into a row: the list fills the nodes in when it is read, so a notification always shows a post as it is now.
One row for one thing. The unique key one_of_each (recipient, type,
subject, object, target and comment_id) keeps a repeated event from
making a second row. Telling somebody the same thing again moves the row to
the top of their list, unread: an event moved twice says so once.
The kinds
type |
Told when | Who is told |
|---|---|---|
note_add, article_add, photo_add, topic_add, video_add |
A profile posts | Its subscribers who may see the post |
reply_add |
A reply is made | The author of the post at the top of the thread, the author of what it answers, and the post's subscribers, each when they may see the thread and the reply |
quote_add |
A note quotes a post | The quoted post's author, when they may see the note |
repost_add |
A post is reposted | Its author |
voteup |
A post is liked | Its author |
actor_mention, actor_mention_reply |
A post or a reply names a person | The person, when they may see it (Permissions) |
actor_tag_request |
A person is tagged in a photo or a video, and the tag waits for them | The person, when they may see the post (Permissions) |
actor_tag |
A person is tagged at once, as they chose | The person, likewise |
actor_tag_accept |
A person approves being tagged; its subject is them | The post's author |
actor_follow |
A profile is followed | The person, or a group's or an event's administrators |
actor_follow_request, actor_follow_request_accept |
Somebody asks to follow, and is let | The person or the administrators; then the one who asked |
actor_invite, actor_invite_accept |
Somebody is invited to follow, and accepts | The one invited; then the one who invited |
admin_add |
Somebody becomes an administrator of a profile | Its other administrators |
event_rsvp |
Somebody answers an event | Its administrators |
event_changed, event_cancelled, event_reminder |
An event moves, is called off, or is near | Each person who answered it |
Subscribers. Following a profile, or becoming one of its administrators, subscribes to it; each is a reason to stay subscribed, and the subscription ends with the last of them. Get notifications, in a post's menu, subscribes to that post: its replies are told.
Only what its reader may see
Nobody is told of a post they may not see, and nobody is shown one:
- When a notification is made, each recipient is asked about, as a
registered member, with
permissions.CanReadEach: the post, and the reply that holds what was done. A post for a profile's followers is told to its followers, not to everybody subscribed; one for its administrators, to its administrators. Asked as a member, a site administrator is told what they would be as one. - When the list is read, the viewer is asked about every post and reply in it at once. A notification about one they may no longer see (it was made private, they left its audience, or it is gone) is left out and deleted, so that the unread count says what the list does. When the question cannot be answered, the list is empty rather than unchecked.
- A block ends the follows and the subscriptions between the two, both ways, and what each was told of the other goes, with what a group's or an event's administrators were told of the one it blocked.
The rule is the same one every post is read by, permissions.CanRead, asked
by id through the graph's FilterVisible. A test keeps the two in step:
graph-grpc-service/data/repositories/visibility_agreement_integration_test.go.
Who is never told
- Anybody, of an edit. Changing a post tells nobody (the user, 2026-10-19); a name an edit adds is a mention, and told as one.
- Whoever did it, of their own doing.
- A profile on another server, of a like, a repost, a reply, a quote, a mention or a follow: its own server is told, by federation.
- Of a draft. Nothing is said of a post until it is published.
- Of the notes after the first of a thread written in one go
(
POST /notes/…/thread): whoever is told of the thread is told of its first note, once (partin the event; roadmap row 101). A name mentioned in any of its notes is still told, as a mention.
When what it tells of goes
A notification that leads nowhere is noise, so it goes with what it tells of, for everybody who was told, whoever's hand removed it:
| What goes | What goes with it |
|---|---|
| A post or a reply | Every notification naming it as object, comment_id or target: its likes, reposts, quotes, mentions, and the replies under it |
| A repost, taken back | Its repost_add, found by the repost's id in comment_id |
| A like, taken back | Its voteup |
| A name, taken out of a post in an edit | Its actor_mention (actor-service says so, edge.mention.*.deleted.v1) |
| A tag, taken off by the person or the author | Its actor_tag_request, actor_tag and actor_tag_accept (edge.tag.*.deleted.v1) |
| A follow | Its actor_follow, and the subscription unless they administer the profile |
| An administrator | Their admin_add, and the subscription unless they follow the profile |
| A note from another server, taken back there | Everything about it |
| An account, purged | Everything to, by or about it (graph-grpc-service's purge) |
| A block | What the two were told of each other |
Taking one of these back tells nobody.
Choosing what one is told about
Everybody is told about everything to begin with. A person turns kinds of
thing off, and on again, in the Notifications section of their settings
(GET and PATCH /notifications/settings); a notification of a kind turned
off is not made at all. Each off is a row in notifications_off; on is
none. A purge takes them with the person.
Kind of thing (topic) |
Notifications |
|---|---|
likes |
voteup |
reposts |
repost_add, quote_add |
replies |
reply_add |
mentions |
actor_mention, actor_mention_reply, actor_tag_request, actor_tag, actor_tag_accept |
follows |
actor_follow, actor_follow_request, actor_follow_request_accept |
posts |
note_add, article_add, photo_add, topic_add, video_add |
events |
event_rsvp, event_changed, event_cancelled, event_reminder |
invitations |
actor_invite, actor_invite_accept, admin_add |
Several as one
Likes of one post, reposts of one post, and follows of one profile are one
line of the list: "Worf and 2 others liked your post"
(notification-service/cmd/api/handlers/group.go). They are grouped on each
page of the list, at the place of the newest; each row stays as it was
stored. The line carries every row's id (ids), whoever else did the same,
newest first (others, at most two), and how many more (others_count).
It is unread while any of its rows is, and reading or deleting it reads or
deletes them all. A line of several follows offers no + Follow.
What can be done from one
The list says, for each notification, what its reader can still do from it
(actions, worked out as it is read, from the graph as it is now; never
stored: notification-service/cmd/api/handlers/actions.go):
actions |
On | When |
|---|---|---|
answer_request |
actor_follow_request |
The request is still waiting: accept or decline it |
answer_tag |
actor_tag_request |
The tag is still waiting: accept (PUT /people/:id/tags/:post_id) or decline (DELETE) |
follow |
actor_follow |
The reader does not follow whoever followed |
reply |
reply_add, actor_mention, actor_mention_reply, quote_add |
Always: answer the reply (comment_id), or the post |
An event's notifications have none: whoever is told has answered the event already, and changes the answer on its page. Using an action reads the notification.
Read, and how long they are kept
A notification is read when its reader opens it, by following one of its links, or uses one of its actions; or with Mark all read. Seeing it in the list does not make it read.
How long they are kept
A read notification is removed NOTIFICATIONS_KEEP_DAYS days after it was
read: 90 unless the installation sets another number, and never with 0
(Configuration). An unread one stays until
it is read. notification-service looks for them when it starts and every six
hours after, and removes them a thousand at a time
(notification-service/cmd/api/keeping.go).
Delivery
Events are acknowledged as they arrive and never delivered twice, so nothing is written or sent twice; a handler that fails says so in the log, and an event published while notification-service is down is not heard (Deploying). Keep notification-service at one copy: each copy hears every event.
The API
| Route | What it does |
|---|---|
GET /notifications/?start=&limit= |
The viewer's notifications, newest first |
GET /notifications/unread-count |
How many they have not read (count), and when the newest was told (newest_at) |
GET /notifications/settings |
What the viewer is told about: every kind of thing, on or off |
PATCH /notifications/settings |
Turns one kind of thing on or off (topic, on) |
PATCH /notifications/read-all |
Marks all of them read (Mark all read), and answers how many were unread |
PATCH /notifications/:id |
Marks one read |
DELETE /notifications/:id |
Deletes one |
POST /notifications/subscriptions |
Subscribes to a profile or a post |
DELETE /notifications/subscriptions/:target_id |
Ends that subscription |
Every route is the viewer's own: nobody reads, marks or deletes another's notifications. The reference is api/notification-service.json (opens in a new tab).
From anahitasocial/anahita-services, 2026-10-27.
Written by Claude, reviewed by Rastin Mehr · About this