Skip to main content
Per-user activity feed. Other handlers create rows by calling the internal CreateNotification helper (internal/api/notification_handler.go); the user-facing endpoints below let the frontend list, count, mark read, and dismiss them. Every new row also fans out over WebSocket on the user:{userID} channel as notification.created so the dashboard updates without polling.
All endpoints below require an authenticated session. Notifications are strictly per-user — every query scopes to user_id = <session user>, and a row belonging to another user is indistinguishable from one that doesn’t exist (404).

Endpoints

MethodEndpointPurpose
GET/api/v1/notificationsList notifications, newest first
GET/api/v1/notifications/countUnread count for the navbar badge
POST/api/v1/notifications/{notificationId}/readMark one notification read
POST/api/v1/notifications/read-allMark all unread read
DELETE/api/v1/notifications/{notificationId}Delete one notification

Response shape

FieldTypeNotes
idstring (CUID)Notification id
actor_type"user" | "agent"Who triggered the notification
actor_idstringUser or agent id
actor_namestring | omittedJoined from users.full_name or agents.name; the field is omitted (carries omitempty) when the actor row no longer exists
actionstringFree-form action verb (e.g. mentioned, assigned, completed)
entity_typestringEntity kind the action targets (e.g. mission, issue, chat)
entity_idstring | nullEntity id
entity_titlestring | nullCached title; safe to display without joining
read_atRFC3339 | nullnull = unread
created_atRFC3339Always set

GET /api/v1/notifications

Paginated list, newest first.
Query paramTypeDefaultNotes
limitint50Capped at 100 server-side.
offsetint0
read"true" | "false"(unset = all)Filter by read state.
Returns 200 with a JSON array (possibly empty — never null).
curl -H "Cookie: <session>" \
  "https://crewship.example.com/api/v1/notifications?read=false&limit=20"
[
  {
    "id": "ntf_01HVZ...",
    "actor_type": "user",
    "actor_id": "u_01HVY...",
    "actor_name": "Petra Nováková",
    "action": "mentioned",
    "entity_type": "mission",
    "entity_id": "mis_01HVX...",
    "entity_title": "Q4 onboarding refresh",
    "read_at": null,
    "created_at": "2026-05-20T08:14:22Z"
  }
]

GET /api/v1/notifications/count

Unread count, cheap. Used to drive the navbar badge.
{ "unread": 7 }

POST /api/v1/notifications/{notificationId}/read

Mark a single notification read. Idempotent — re-reading an already-read notification still returns 200. Returns 404 only when the notification does not exist (or belongs to a different user, which is indistinguishable on purpose).
{ "status": "ok" }

POST /api/v1/notifications/read-all

Mark every currently-unread notification read for the calling user. Returns the row count.
{ "updated": 23 }

DELETE /api/v1/notifications/{notificationId}

Hard delete. Returns 204 No Content on success, 404 when the row doesn’t exist or doesn’t belong to the calling user.

WebSocket event

When any handler in the server calls CreateNotification, the new row is broadcast on the user:{userID} channel:
{
  "type": "notification.created",
  "channel": "user:usr_01HVZ...",
  "payload": {
    "id": "ntf_01HVZ...",
    "action": "mentioned",
    "entity_type": "mission",
    "entity_id": "mis_01HVX...",
    "entity_title": "Q4 onboarding refresh"
  }
}
The frontend listens on its own user channel — see WebSocket for the connect + subscribe protocol.

See also

  • Inbox — the user-facing UI on top of notifications + assignments.
  • WebSocketnotification.created channel format.
  • Crew Journal — system-level event stream (notifications are downstream of journal entries).