Notifications
Distribu surfaces business events two ways inside the dashboard:
- In-app notifications — a per-user, per-company stream of
actionable events (new orders, status changes, returns, team changes,
etc.), delivered to a bell icon in the header with an unread
count and dropdown. Full list at
/dashboard/notifications. - Activity feed — a lighter, company-wide feed on the dashboard home, rendered from the audit log. Broader than notifications (includes things like CSV imports, product edits) but not actionable.
Notifications are per-recipient: when an event fans out to three staff, three rows are written — each with their own read state. Marking one read doesn't affect anyone else.
The event types
Thirteen notification types fire today:
| Type | Fires when… |
|---|---|
ORDER_PLACED | A new order is placed (storefront or API). |
ORDER_STATUS_CHANGED | An order transitions between statuses. |
ORDER_LOW_STOCK | An order placement pushed a product below its low-stock threshold. |
CUSTOMER_REGISTERED | A customer self-registers on your storefront. |
MEMBER_INVITED | A staff invite is sent. |
MEMBER_JOINED | An invited staff member accepts and joins the team. |
MEMBER_REMOVED | A staff member is removed from the team. |
MEMBER_ROLE_CHANGED | A staff member's role changes. |
SCHEDULED_REPORT_FAILED | A scheduled report dispatch fails. |
SUBSCRIPTION_PAST_DUE | Stripe reports a failed invoice payment. |
SUBSCRIPTION_CANCELED | Your subscription is canceled. |
RETURN_REQUESTED | A staff member opens a new return. |
REFUND_PROCESSED | A refund is issued against a return. |
Role fan-out
Not every event goes to every staff member. Who receives what depends on the event type:
| Event types | OWNER | ADMIN | MEMBER |
|---|---|---|---|
ORDER_PLACED, ORDER_STATUS_CHANGED, ORDER_LOW_STOCK, CUSTOMER_REGISTERED, RETURN_REQUESTED | ✅ | ✅ | ✅ |
REFUND_PROCESSED, MEMBER_INVITED, MEMBER_JOINED, MEMBER_REMOVED, MEMBER_ROLE_CHANGED, SCHEDULED_REPORT_FAILED, SUBSCRIPTION_PAST_DUE, SUBSCRIPTION_CANCELED | ✅ | ✅ | — |
Day-to-day operational events (orders, returns, low stock, new customers) reach everyone. Sensitive / administrative events (team changes, billing, refund decisions, report failures) stay with OWNER and ADMIN.
Actor exclusion
If the person who triggered the event is a staff user, they're excluded from their own notification — no point telling you you just did a thing you did. Customer-triggered events (storefront order placement, self-register) and system-triggered events (Stripe webhooks, cron failures) don't exclude anyone; every staff member in the role set gets notified.
The bell icon
In the dashboard header, a bell icon carries a red badge with the unread count. The badge:
- Hides entirely at 0.
- Shows "99+" when the unread count exceeds 99.
- Updates via polling — the client calls
GET /api/notifications/countevery 60 seconds. - Pauses polling when the tab is backgrounded (Page Visibility API). When the tab becomes visible again, one immediate refresh fires, then 60-second polling resumes.
Clicking the bell opens a dropdown with the 10 most recent
notifications. Each row shows an icon, title, body, relative
timestamp, and is clickable — click navigates to the event's
destination (e.g., the order detail page for ORDER_PLACED) and
marks the notification read optimistically.
A "Mark all as read" button sits at the footer of the dropdown, and "View all" links to the full page.
The full list
Navigate to /dashboard/notifications for
a paginated list (25 per page) with All / Unread tabs. Each
row has an inline Mark read action; a header-level Mark all as
read button clears everything with one click.
Notification payload
Each notification row stores:
| Field | Type | Notes |
|---|---|---|
type | enum | One of the 13 types above. |
title | string | Short headline — e.g., "New order #A1B2C3D4". Denormalised at write time so renames of referenced entities don't retroactively change notification history. |
body | string | One-line description. |
href | string, optional | Where clicking the notification navigates. Null for events with no destination. |
metadata | JSON | Type-specific structured data (order ID, customer name, from/to statuses, etc.). |
readAt | timestamp, optional | When the user read it. Null means unread. |
createdAt | timestamp | When the notification was created. |
Multi-company users
Notifications are scoped to (userId, companyId). A user who's a
member of two companies sees only the current session's company's
notifications in the bell — switching companies switches the stream.
API
Four routes back the bell + list UI. All require an authenticated
dashboard session (cookie-based) with role OWNER, ADMIN, or
MEMBER.
| Method | Path | Purpose |
|---|---|---|
GET | /api/notifications | Paginated list. Query: limit (1–100, default 25), cursor, filter=unread|all. Returns { items, nextCursor, unreadCount }. |
GET | /api/notifications/count | Cheap unread-count poll. Returns { unreadCount }. This is what the bell polls every 60 s. |
POST | /api/notifications/[id]/read | Mark one read. Returns { ok, unreadCount }. 404 if the notification isn't owned by (userId, companyId). |
POST | /api/notifications/read-all | Mark all current-company notifications read. Returns { ok, markedCount }. |
These are session-authenticated (not API-key). They're not documented in the REST API section because they're for the dashboard UI itself.
The activity feed
Separately from notifications, the dashboard home page shows a company-wide activity feed rendered directly from the audit log. It covers a superset of events (including CSV imports, product edits, API-key creation, etc.) but it's read-only — no unread state, no per-user scoping.
Use the activity feed when you want a quick "what's been happening"; use notifications when you want to act on something.
Email digest
Optionally, Distribu will roll up your unread notifications into a daily or weekly email so you don't have to stare at the bell all day. Off by default; each user opts in per company.
See Digest emails for cadence, timing, and configuration.
Next: Digest emails.
