Notifications — the bell, email, and who decides what reaches you
In plain terms: the platform tells you when something happens to you — you were assigned a task, someone offered you a payout, an administrator opened your account. It arrives in the bell at the top of every page, and by email when you are not in the app.
The rule that keeps the bell worth opening is narrow on purpose:
A notification is created only when the event concerns you directly.
Everything else — every task anyone created, every project anyone edited — stays in the Activity feed, which is exhaustive by design. The bell is not a smaller activity feed; it is a different question. If they were merged, the bell would become something you scroll past, which is the one failure that makes the whole thing useless.
You are also never told about something you did yourself.
How to...
Your notifications and settings
See what you have missed — the bell in the top-right corner of any page. The number on it counts notifications you haven't read yet. Click a notification to go straight to whatever it's about. Unread ones are grouped into a collapsible tree by where they lead (Tasks, Departments, Projects, Roles, ...), collapsed by default so opening the bell never shows more than the group headers until you expand one — each group's own count is the true total, not just however many happened to load. Mark as read on a row sits away from where you click to open it, so you can't misfire one for the other. A group's own mark read reaches every unread row behind it, not just whatever happened to be loaded. Opening the bell itself marks nothing read — only clicking a row or a mark-read control does.
Read the full list — bell → All notifications, or My Account → Notifications. Filter by status (unread/read) or by importance, and mark anything read or unread again. The same destination-grouped tree is available here too, behind a toggle (off by default — see Your notifications below for why flat, not grouped, is the safer starting view here).
Stop a kind of notification from emailing you — My Account → Notifications → Settings tab → turn off Email for that group. It keeps appearing in the bell; only the email stops. See field guide.
Change the language your emails arrive in — My Account → Profile → Language, not the avatar menu's own EN/RU/HY switcher (2026-08-21 — see IAM → Account for the full split). That switcher only changes what you see for the current session and was found silently changing people's permanent email language too, which is exactly why the two were separated; only the profile page's own field is your permanent default. Text inside the app itself still always follows whichever language is open right now — the avatar menu's switcher, if you've used it this session, otherwise your profile default.
Platform policy (administrators)
Change what the platform sends — Platform Administration → Notifications. Each group can be switched off entirely, given a different importance, have email on or off by default, and be locked so people cannot change it for themselves.
Pause all outgoing email — same page, the switch at the top. In-app notifications keep working and queued emails are kept — they go out when you switch it back off, so nothing is lost.
Find out why someone is not getting an email — the same page shows a person's effective settings, read-only. You cannot change them for them; see Who decides what for why.
Organization policy (Team Manager/Team Admin)
Change what this organization's own product groups send — Settings → Notifications (only present for an organization subscribed to a product with notification groups). Same Enabled/Importance/Email-by-default/Users-may-change controls as Platform policy above, but scoped to this organization's own groups — for P4P Internal, the Team Management and Calendar ones. See Organization policy below for who can reach this page.
The groups
You configure groups, not individual events. There are far more events than groups, which is deliberate: the settings screen stays readable, and adding a new event to an existing group changes nothing you have to look at again.
| Group | What it covers | Importance | Email by default |
|---|---|---|---|
| Account security | sign-in from a new device, password change, an administrator opening your account | Important 🔒 | on, cannot be turned off |
| My tasks | you were assigned, removed, or a task you work on was deleted | Normal | on |
| Status of my tasks | the status of a task you work on changed | For information | off |
| Comments | new comments on a task or a project discussion you are part of | Normal | on |
| Payouts | offers, answers, and payment confirmations | Important | on |
| Projects, departments and roles | added to/removed from a project or department; a platform role assigned, revoked, renamed or deleted | Normal | on |
| Administration | new HR pool applications (only if you can process them) | Normal | on |
| Calendar invitations | invitations, reschedules, cancellations and answers to events you are on | Normal | on |
| Event reminders | a nudge shortly before an event starts | Normal | off |
| Department activity | departments created, edited or deleted, and roster or project changes in yours | For information | off |
| Project activity | projects created, edited or deleted | For information | off |
| Team members | a colleague joining or leaving the team | For information | off |
| Announcements | platform-wide announcements from P4P administrators | Normal | on |
| Agent runs | a background or scheduled agent task you started finished or failed | Normal | off |
| Support | a request for help you opened, and replies on it — or, if you answer them, one arriving | Normal | on |
| Time off | a time-off request waiting for your decision, the decision on yours, a colleague's sick leave or cancelled time off | Normal | on |
| Platform role permissions | a permission was added to or removed from a platform role you hold | Normal | off |
| Subscription changes | a product subscription was activated or deactivated for an organization you administer | Normal | on |
Only groups that can actually produce something are shown to you. If an administrator switches one off, it disappears from your settings rather than sitting there as a toggle over nothing.
Three levels of importance
Importance is not a colour. It decides how a notification reaches you, and whether you may decline it:
- Important — in-app and email, always. Account security cannot be turned off at all: someone signing into your account is not a matter of taste.
- Normal — in-app always, email on unless you turn it off.
- For information — in-app, no email unless you ask for one.
In the list, only Important carries a visible marker. If all three were coloured, every row would be coloured and none would stand out.
What always happens, whatever you set
- In-app is never switchable off. The notification is the record that the thing happened; if you mute a group, you still need somewhere to find what you muted. "Email or not" is the only real question, which is why it is the only one the settings screen asks.
- Repeats collapse. Three comments on one task are one row saying "×3", not three rows — and one email, not three. Account security is the exception: each of those is its own incident and must survive on its own.
- You are never told about your own actions.
- Changing policy does not rewrite the past. A notification keeps the importance it had when it was created.
Who decides what
| You | Platform administrator | |
|---|---|---|
| Your own email settings | ✅ change them | 👁 read them only |
| Whether a group exists at all | — | ✅ |
| A group's importance | — | ✅ |
| Whether people may change a group | — | ✅ (may lock, never unlock account security) |
| Pause all outgoing email | — | ✅ |
Nobody can edit your personal settings — not even a Super Admin. Everything an administrator legitimately needs is reachable through policy: to reach everyone, they lock a group on; to reach nobody, they switch it off. The outcome is achievable without the access, so the access does not exist.
They can see your effective settings, because otherwise nobody could answer "why am I not getting the email" — the one question this system reliably produces.
Two things policy cannot do:
- Unlock account security. It may always tighten, never loosen.
- Turn in-app off. See above.
Email
Email is sent after the action, by a background job, never inside the request that caused it. A task can therefore never fail to be created because the mail server is unreachable — the message waits in a queue and goes out when it can.
Practical consequences:
- An email can arrive up to about a minute after the thing it is about.
- Pausing email does not lose anything; it delays it.
- Emails are written in the language stored on your profile, not the language of whoever caused the notification.
- Every email carries a link to your notification settings.
Your notifications (/account/notifications)
In plain terms: everything addressed to you, and the only settings that are yours to change.
Two tabs. Notifications is the list — filter by status or importance, open anything to go where it happened, mark it read or unread again. Settings is one row per group, with a single question each: should this also reach my inbox.
The list shows read notifications too, dimmed rather than removed. Reading something is not throwing it away, and a list that deletes what you just clicked makes a misclick unrecoverable.
Flat vs. grouped (2026-08-13) — a toolbar toggle switches the list between flat (default) and the bell's own destination-grouped tree. Flat is the safer default here specifically: a collapsed group renders almost no height, so infinite-scroll's own trigger would otherwise sit inside the viewport with nothing to actually scroll and silently front-load your entire history the moment the page opens. Switching to grouped view pauses further auto-loading until you open at least one group; flat view is never affected by this.
Notification settings — field guide
My Account → Notifications → Settings
- Email (per group) — whether this group also reaches your inbox. In-app is not listed because it is not a choice.
- A lock icon next to a group means platform policy fixed it. The tooltip says who set it. The only permanently locked group is Account security.
- Groups an administrator disabled are not listed at all — and neither are groups from a product you have no access to (2026-08-18): the Team-Management-flavoured groups only appear if you hold
workspace:staffing:read, and Calendar's two only if you holdworkspace:calendar:read(both renamed fromplatform:staffing:read/platform:calendar:read2026-08-18,docs/staffing/ADR-002- org-rbac-migration.mdStage 8). Account security is the one group everyone always sees.
Platform policy (/platform/notifications)
In plain terms: what the platform sends, to whom, and how loudly — for everyone at once. As of 2026-08-19, that means exactly one group: Account security, the only one belonging to no product. Every other group's own policy moved to the organization that actually owns it — see Organization policy below.
Requires platform:notifications:read to see and platform:notifications:manage to change. Everything here affects notifications created from now on; nothing already sent is rewritten.
Narrowed, 2026-08-19 — no longer reaches product-scoped groups. Until this date these same two permissions also governed the platform-wide "ceiling" over the Team-Management-flavoured groups and Calendar's two — but with exactly one organization (P4P Internal) ever subscribing to that product, a platform administrator configuring something only that one organization would ever see was an org concern wearing a platform permission (docs/iam/PERMISSIONS_CATALOG.md's own dated entry). Every product-scoped group's policy is now owned entirely by its organization; nothing above it narrows or overrides it any more.
Platform policy — field guide
Platform Administration → Notifications (platform:notifications:manage)
- Enabled — off means the group produces nothing, for anyone. It also disappears from everyone's personal settings.
- Importance — see above. Applies to notifications created from now on.
- Email by default — what someone gets before they have expressed any preference. Changing it reaches everyone who never touched their own setting, and nobody who did.
- Users may change — off locks the group to the default above. Account security is permanently locked and cannot be unlocked here — the only group left on this page, so in practice this control is now always disabled.
defaultbadge — that group is running on the built-in value; nobody has overridden it.- Pause all outgoing email — the operational pause. There is also a separate
NOTIFICATIONS_EMAIL_ENABLEDenvironment variable, and both must allow sending. If the variable is blocking, this page says so — otherwise pausing and seeing no change would be a mystery.
Organization policy (/org/[slug]/notifications)
In plain terms: the same idea as Platform policy above, but scoped to one organization's own product-scoped groups — for P4P Internal, its Team-Management and Calendar ones. Requires workspace:notifications:manage, granted only to P4P Internal's own Team Manager/Team Admin roles (not the generic system OWNER/ADMIN — same rule workspace:staffing:*/ workspace:calendar:* already follow). Only ever reachable for an organization that actually subscribes to a product with notification groups; the nav entry (and the page itself) is simply absent otherwise.
Same field guide as Platform policy above — Enabled/Importance/Email by default/Users may change, per group — this screen fully owns its own groups rather than narrowing a platform ceiling above it (that ceiling doesn't exist for these groups any more, see above). Superseded a two-column "platform value beside organization's own narrow-only value" design the same day it shipped (docs/staffing/ADR-003-product-scoped-team-management.md) — with nothing above this layer left to compare against, a single fully-editable table is the whole story.
Where notifications come from
Nothing here is its own feature. Each notification is produced by an ordinary action somewhere else in the platform, and each of those pages describes what it sends in its own Notifications section:
- Tasks, projects, departments, HR pool — Staffing
- Payouts — Finance
- Calendar events and reminders — Calendar
- Account security — IAM
- Background and scheduled agent runs — AI
Notifications
This page itself sends nothing. It is where you read and configure everything else.
Changing a setting here takes effect immediately, and only for notifications created afterwards — anything already in your list keeps the importance and channels it was created with.
Testing this module
The backend behind /platform/notifications — importance freezing onto a notification at send time (not rewritten retroactively), a disabled group producing nothing at all, locking taking a user's own opt-out choice away and restoring it intact on unlock, account security's lock being un-liftable even by an administrator, the email pause and its interaction with the NOTIFICATIONS_EMAIL_ENABLED environment switch — is thoroughly exercised, API-only (no browser), by scripts/e2e/notifications/policy.mjs. The bell/email side of the system (deduplication, actor exclusion, cross-user isolation, new-device/impersonation/password-reset security alerts, sidebar badges) has its own permanent suite — see scripts/e2e/README.md's own notifications/ section.
Nothing had ever driven /platform/notifications's actual browser UI until this round: apps/portal/pages/platform/notifications.test.ts (9 tests) covers the switches/selects needing platform:notifications:manage to be enabled at all, patch()'s "replace the row with exactly what the server resolved" contract (never echo the optimistic value — the policy layer may refuse part of a request), setPaused()'s optimistic-update-then-revert-on-failure, the environment-disabled alert, account security's userCanOverride switch staying disabled even WITH manage permission (unlike an ordinary group's), and the "default" badge only showing for a policy nobody has overridden. scripts/e2e/menu/platform-admin.mjs extended with a live policy-table render check and a reversible "Email by default" toggle-and-restore round trip.
Found and fixed, 2026-08-19/20 (was: "found, not fixed" — the middleware name in that original writeup was itself stale by the time it was written, see below): platform/notifications.vue's definePageMeta was missing middleware: 'platform-admin-access', the one thing every other /platform/* page has and the only thing that populates profile/platformPermissions via useCurrentUser().ensureLoaded() on a fresh load. (The original bug report named the OLDER platform-access middleware, superseded by platform-admin-access back on 2026-08-14 — that older file turned out to be fully dead code by 2026-08-19, unreferenced by any page any more, and was deleted the same day the correct middleware got added here.) A fresh navigation — a bookmark, a browser refresh, typing the URL, opening a new tab — used to render the entire policy table with every control disabled, even for a SUPER_ADMIN; it only worked after first visiting some OTHER /platform/* page in the same session (whose own middleware warmed that shared state) and navigating here via an in-app link, which is exactly why normal sidebar-driven manual testing never surfaced it. The permanent e2e check that used to pin the buggy behavior now asserts the corrected one instead.
/account/notifications (the personal list page — had zero coverage, unit or live, before 2026-08-12) — the bell/badge live-update MECHANISM is already thoroughly proven elsewhere (notifications/{in-app,badges}.mjs, two real accounts, sharing this exact page's own composable state); scripts/e2e/menu/account.mjs (round 3 of the /account/* series, 11 checks total) covers what those don't — the page's own tab/filter UI and a real per-row toggle: it loads with real notification history (this session's earlier rounds already left plenty, no seeding needed), switching to Settings updates the URL to ?tab=settings (that tab's own content is the Settings section above, already covered by notifications/{groups,policy,security,email}.mjs), the "Unread" status filter narrows to only rows offering "Mark as read" — the row's own available ACTION, not "Mark as unread" (a real gotcha caught live: the button names what clicking it would DO, so an unread row reads "Mark as read," easy to get backwards writing the assertion) — and a real read/unread toggle round trip on one row, restored afterward so the run leaves history unchanged. Companion unit coverage, apps/portal/pages/account/notifications.test.ts (12 tests) — tab switching via the URL, the "unread" filter triggering a real server reload vs. "read"/importance staying purely client-side, activeFilterCount counting only importance (status has its own always-visible control, so counting it too would show a number nothing in the filter menu explains), the three distinct empty/error states, onActivate marking a row read but never re-marking an already-read one unread, selection scoped to the currently FILTERED rows (not the whole loaded list — "select all" under a filter can't silently reach past what's on screen) and clearing on any filter change, and the real bulk setReadMany call. NotificationRow (shared with the bell's own popover) is mounted for real; NotificationSettings stays stubbed, matching the e2e split above.