Hackorda Docs
Flows

F 13 Notifications

Status: ๐ŸŸข ready to test Owner: Adilet Last updated: 2026-05-08 Last verified on production: โ€”

Goal

Every user has an in-app bell that surfaces unread notifications, links each to the source resource (issue / cycle), and supports "mark all read". The bell fires for 11 distinct event types across the QA platform.

Actors

  • Primary โ€” Any signed-in user
  • Secondary โ€” Whatever upstream system fired the event (triage, payouts, run-batch, comment thread, role assignment)

Preconditions

  • The user is signed in.
  • An upstream event has fired that should notify this user (e.g. an admin triaged their issue, they were added to a cycle, etc.)

Trigger

The user clicks the bell icon in the sidebar.

Notification event types

TypeWhen it firesAudienceSource flow
issue_filedTester submits a new issueCycle leads + admins (excl. reporter)F-08
issue_approvedAdmin approves in triageReporterF-09
issue_rejectedAdmin rejects in triageReporterF-09
issue_paidAdmin pays an issue (any path)ReporterF-11
issue_reclassifiedAdmin changes severityReporterF-09
issue_awaiting_verification(reserved โ€” not yet wired)ReporterF-10
issue_verifiedAdmin/lead marks verifiedReporterF-10
issue_info_requestedAdmin uses Request Info in triageReporterF-09
issue_info_providedReporter replies on info-requested issueCycle leads + admins (excl. reporter)F-09
cycle_status_changedCycle activates/closes/etc.All cycle members (excl. actor)F-05
tester_assignedAdmin adds a tester to a cycleThe newly assigned testerF-05

Happy path โ€” read + mark read

#ActorUI surfaceActionAPI callDB writesSide effectsSuggested event
1Usersidebar bellOpen dropdownGET /api/me/notificationsโ€”โ€”notification.viewed
2FrontenddropdownRender up to N rows newest-first; unread highlightedโ€”โ€”โ€”โ€”
3Usera rowClickPATCH /api/me/notifications/:id body { readAt: NOW } (or via mark-on-click)UPDATE notifications SET read_at = NOW() WHERE id = :idnavigate to linked resourcenotification.opened
4UserdropdownClick "Mark all read"POST /api/me/notifications/mark-all-readUPDATE notifications SET read_at = NOW() WHERE user_id = :uid AND read_at IS NULLbell badge clearsnotification.mark_all_read

Notification creation pattern

Every upstream API handler calls createNotification(...) or createNotificationsBulk(...) from src/lib/notifications.ts after the primary write succeeds. Failures are swallowed and logged โ€” a notification table outage cannot block the primary action.

Self-notify is suppressed: actions an actor takes on their own resources do not generate a row addressed to themselves.

The bulk helper deduplicates by (user_id, type, issue_id, cycle_id) so the same person never gets two copies of one event (e.g. an admin who is also a cycle lead).

Acceptance criteria

  • AC-1 โ€” Given a tester files an issue, when the POST returns, then leads + admins of the cycle (excluding the reporter) each have a new notifications row with type='issue_filed', cycle_id + issue_id set.
  • AC-2 โ€” Given the reporter replies on an info_requested issue, then leads + admins are notified via issue_info_provided, and the reporter is not in the audience list.
  • AC-3 โ€” Given the user opens the bell dropdown, when there are 0 unread, then the bell badge is hidden.
  • AC-4 โ€” Given a user has 5 unread notifications, when they click "Mark all read", then all 5 rows have read_at set and the badge disappears.
  • AC-5 โ€” Given a notification has cycle_id + issue_id, when the user clicks the row, then they navigate to /app/test-cycles/:cycleId/issues/:issueId.
  • AC-6 โ€” Given the upstream notification insert fails (DB error), when the primary action succeeded, then the user still sees the primary success path; the failure is console.warn'd.
  • AC-7 โ€” Given an admin runs a period batch and pays 10 issues from 3 testers, then exactly 10 issue_paid rows are inserted, distributed across the 3 reporters; no admin gets a row for issues they paid themselves.

Test data / fixtures

  • Baseline seed
  • Two users: an admin and a tester member of an active cycle

Negative paths

#ScenarioExpected behavior
N-1Notifications dropdown opens with thousands of rows (long-lived account)Pagination or cap at 50 newest-first; verify limit
N-2User clicks a notification linking to a deleted issueAPI 404, dropdown does not crash; offer to mark-read-and-dismiss
N-3Two admins fire approval on different issues at exact same timeBoth notifications insert independently; no race
N-4DB insert failsLogged via console.warn; primary action succeeds; user gets no bell ping (graceful)

Manual QA checklist

  • As tester, file an issue (F-08)
  • As admin, sign in โ†’ bell badge shows "1"; dropdown shows "Issue filed"
  • Click the row โ†’ navigate to issue detail
  • Refresh bell โ†’ that row is now "read" (not bold)
  • As admin, approve the issue โ†’ reporter's bell badge shows "1"
  • As reporter, click "Issue approved" notification โ†’ land on issue detail
  • As admin, run a period batch (F-11)
  • As reporter, bell shows "Paid: " with method + reference body
  • In bell dropdown, click "Mark all read" โ†’ badge clears, all rows greyed
  • Verify notifications table: read_at populated for all the user's rows

Automated test outline

test.describe("F-13 notifications", () => {
  test("issue.filed โ†’ leads+admins notified, not reporter", async ({ request }) => { โ€ฆ });
  test("self-notify suppressed", async ({ request }) => { โ€ฆ });
  test("dedup across multi-role users", async ({ request }) => { โ€ฆ });
  test("mark-all-read clears badge", async ({ page }) => { โ€ฆ });
  test("graceful insert failure", async ({ request }) => { /* simulate DB issue */ });
});

Code references

  • UI: sidebar bell button + dropdown (find via useNotifications hook usage; lives in the QA section sidebar)
  • API:
    • src/app/api/me/notifications/route.ts (GET list, POST/DELETE bulk?)
    • src/app/api/me/notifications/[id]/route.ts (PATCH read)
    • src/app/api/me/notifications/mark-all-read/route.ts
  • Hooks: src/hooks/test-cycles/notifications.ts
  • Lib: src/lib/notifications.ts (createNotification, createNotificationsBulk, leadAndAdminAudience, cycleMemberAudience)
  • Schema: notifications, NOTIFICATION_TYPES

Events emitted (proposed)

  • notification.created โ€” fired implicitly inside each upstream flow (via the createNotification(s)* helpers). Telemetry can hook the helper itself once we wire a track().
  • notification.viewed โ€” { user_id, unread_count } (when dropdown opens)
  • notification.opened โ€” { user_id, notification_id, type } (when row clicked)
  • notification.mark_all_read โ€” { user_id, marked_count }

Open questions / known gaps

  • No real-time push (no WebSocket / SSE) โ€” bell refreshes on tab focus / poll. Verify the polling interval; if 0, only refreshes on navigation.
  • No email delivery (deferred โ€” feature-matrix.md ยง3.7).
  • No Telegram delivery (deferred).
  • issue_awaiting_verification exists in the enum but isn't wired โ€” could fire when an issue is approved + gate active so the reporter knows their bug is queued for fix re-test.

On this page