trails/openspec/changes/notifications/specs/activity-feed/spec.md
Ullrich Schäfer 95ac79b093 Propose: notifications
Closes the social loop opened by social-feed: a Pending follower has no
way to know their request was approved, and a follower has no signal at
all that someone they follow just posted. Adds a notifications surface
for three v1 event types — follow_request_approved, follow_received,
activity_published — plus a /notifications page, navbar unread count,
and mark-as-read controls.

Capabilities:
- New: notifications (table, page, badge, generation hooks)
- Modified: social-follows (approve + auto-accept emit)
- Modified: activity-feed (public create fans out)
- Modified: journal-landing (nav entry alongside follow-requests)

Design picks:
- Fan-out-on-write for activity_published (1:N) so /notifications is a
  flat single-table query and "mark read" composes trivially. 1:1
  events insert directly. Cost ceiling documented at 10k followers ×
  50 activities/day = 500k/day, still trivial; revisit only if hot.
- Single notifications table with loose subject_id (no per-type FK);
  renderer dereferences by type. Mastodon-style.
- Two distinct nav entries (Follow requests + Notifications). Pending
  is "act on this", notifications is "this happened" — different
  semantics, kept separate.
- Loader-driven unread count (no real-time channel). Real-time is
  deferred.
- 90-day retention for read rows; unread kept indefinitely.

Out of scope: per-type mute preferences, email/push, real-time,
notifications about routes/replies/mentions, federated notifications.
Each tracked as follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:12:35 +02:00

1.1 KiB

ADDED Requirements

Requirement: Public activity creation fans out notifications

Creating an activity with visibility = 'public' SHALL enqueue a fan-out job that inserts an activity_published notification for every accepted follower of the activity owner. The fan-out SHALL run asynchronously so the activity-creation request returns immediately.

Scenario: Public activity fans out

  • WHEN a user with N accepted followers creates an activity with visibility = 'public'
  • THEN a pg-boss job is enqueued, and on completion N notifications exist with type = 'activity_published', recipient_user_id ∈ accepted-followers, actor_user_id = activity owner, subject_id = activity id

Scenario: Private or unlisted activity does not fan out

  • WHEN a user creates an activity with visibility = 'private' or 'unlisted'
  • THEN no fan-out job is enqueued and no notifications are created

Scenario: No accepted followers means no notifications

  • WHEN a user with zero accepted followers creates a public activity
  • THEN the fan-out job runs and inserts zero rows