Skip to content

Telling a user

Your module has something to tell someone: a loan is overdue, an invoice is paid. You write one line, and the kernel handles the channels.

from kewos.notifications import notify_user
from kewos.notifications.user_inbox import NotificationKind
with engine.begin() as conn:
... # your business write
notify_user(
conn,
user_id=42,
kind=NotificationKind.STATE_CHANGE,
payload={"model": "library_loan", "record_id": 19, "state": "overdue"},
)

That is rule 31: no module emits a notification any other way. No hand-rolled email, no private table, no direct call to a push service.

What that line does

ChannelWhen
The inbox in the interfaceimmediately, inside your transaction
A live nudge (WebSocket) on user:42after the commit
A phone notification (encrypted Web Push)after the commit, if the user opted in and the server holds its keys

You declared none of those channels. A user with no browser open and no push finds the notification in their inbox next time they sign in: the channel is a convenience, the inbox is the truth.

The property that matters: a rolled-back transaction tells nobody

notify_user takes your connection, not the engine. The insert therefore joins your transaction. Run:

after a ROLLED-BACK transaction : 1 unread (unchanged)
after a COMMITTED transaction : 2 unread

A failed business write un-notifies the user, because the notification never existed. Same posture as the mail outbox (rule 17): you do not announce something that did not happen.

And both external channels fire on the commit event, not inline:

  • the nudge only says “re-read your inbox”, with no content. A lost race or a rolled-back transaction therefore produces an empty re-read at worst, never wrong information on screen;
  • the push leaves after the commit with a minimal payload (title, body, URL) because it travels through a third party. It is end-to-end encrypted before leaving: the push service reads nothing.

Four kinds, and why the list is closed

MENTION, COMMENT, STATE_CHANGE, SYSTEM. The list is short on purpose: the kind drives the rendering, the grouping and the email digest. One kind per module would make the inbox unreadable and the digest impossible to write.

The payload is free and yours: the kernel does not read it, it renders it.