Notifications
Linear-style notification system shipped in task 45.
Channels
in_app— feed reachable via/api/notifications(GET) + sidebar bell
(rendered by the app shell layout).
email— sent via the shared nodemailer transport (Hostinger SMTP in
prod, Mailpit in dev). Plain text + minimal HTML. MJML templates are a follow-up.
slack,discord— reserved values in the enum so the schema is
forward-compatible; not wired.
Defaults per kind
Critical events (task.needs_review, task.verification_failed, plan.proposed, quota.*, billing.*, security.new_login, member.invited, agent.token_revoked) default to in-app and email at instant frequency.
Chatty events (task.completed, plan.approved, plan.rejected, knowledge.superseded, member.joined) default to in-app only.
Users can override per (kind, channel) in notification_preferences.
Hardcoded overrides
quota.exceeded + security.new_login always email — even if the user opts out — because they protect the user's own budget / account.
Frequency batching
notification_preferences.frequency accepts instant | hourly | daily. v1 dispatch is always instant; hourly + daily are recognized at write time but ignored at dispatch. A digest worker can be added later without a schema change.
Service
notify(db, input, getEmail?) is the single dispatch entry point. Callers (route actions, MCP tools, admin service) pass the event + recipient IDs + an optional getEmail resolver. The function loads preferences in one query, writes the in-app row, and synchronously sends the email.
When the BullMQ worker arrives, swap the sync send for an enqueue — the signature stays the same.
Schema
packages/db/migrations/0017_notifications.sql adds two tables:
notifications— append-only feed; soft-archive viaarchived_at.notification_preferences— per-(user, org, kind, channel)toggle +
frequency.
API
GET /api/notifications→ JSON list of unread for the signed-in user.POST /api/notificationswithop=mark-read&ids=a,b,c→ mark
notifications as read.
Out of scope for v1
- BullMQ email worker — sync send for now.
- MJML templates — plain HTML for now.
- Slack / Discord webhooks.
- Realtime SSE bridge for notification feed — poll-on-focus suffices at
launch volume.