Platform · Notifications

Notification system · states gallery

Initial mock for the durable, pull-based notification queue: events append rows to a per-principal inbox (IcyDB entity, stable memory); the bell in the global top bar shows the unread count and opens the panel. No push, no sockets — the queue is read on page load, which is the IC-native model. Covers V1 only: the in-app queue, flood control (coalescing + per-source cap), the cycle-runway watchdog, and retention. Out of scope (V2): email/push delivery, per-category preferences, cross-kind digests.

Source: Design Documentation/WIP/notification-system-design.md. Related: Top bar · Project cycles / Billing · Navigation.
01Placement · the app-wide top bar (Decision 6)The bell lives in the persistent global top bar on every surface — studio, launchpad, marketplace, landing — whenever a user is logged in. Anonymous visitors get no bell at all (Decision 5): the bar shows Connect instead. The white context bar below keeps only its breadcrumb + current-principal chip; it stops carrying the bell, ending the current drift where each surface copies its own.
Logged in · bell present
Toko LaunchpadMarketplace Studio ◈ 128.40 ICP
Studio/Project: Neon District/Collections
k1m9…7uvw

Bell with its unread badge sits between the divider and the wallet pill. The context bar below carries only the breadcrumb + principal chip.

Anonymous · no bell
Toko LaunchpadMarketplace Connect
Marketplace/Browse

Same bar skeleton, but the bell is gone (Decision 5) and the wallet pill becomes a white Connect button. No principal chip.

02Bell · badge statesThe badge renders unread_count() — a cheap dedicated endpoint, not a feed fetch. No unread → no badge (never a grey zero). Display caps at 9+; the true count lives in the panel. Clicking toggles the panel anchored under the bell.
All read
Unread
Capped

Left to right: no badge at zero (never a grey “0”) · exact count up to 9 · ten or more renders 9+.

03Panel · the queue, newest firstEach row: icon by kind · title · one-line body · relative time. Unread rows carry a blue dot and stronger title weight; read rows are muted. Clicking a row follows its deep-link and marks it read; the × dismisses (deletes) the row. Header: Mark all read; filter All / Unread. Footer: Load more via the IcyDB cursor — the panel paginates, it never fetches the whole queue. The ×12 tally on the claim row is flood-control coalescing (frame 04), not a digest.

Notifications

Cycle runway · 14 days
Neon District is projected to run dry around 30 Jul. Top up in Billing.
2h ago
Claim received ×12
Paper Trail · Corner Shop — 12 claims since you last checked.
3h ago
Vendor empty
Corner Shop sold out — every staged copy has been claimed.
5h ago
Export complete
Generator Vaulties · batch #7 (500 outputs) is ready to download.
Yesterday
You were paid
Royalty payout · 12.40 ICP from a Paper Trail sale.
2d ago
Vendor started
Corner Shop went live on the Launchpad.
3d ago
Creator access granted
Your account can now create projects and collections.
6d ago

Retention never surfaces in the panel — it's housekeeping (frame 07): read rows purge after 7 days, hard cap 50 per principal.

04Flood control · coalescing + per-source cap (V1)Queue protection, not a digest: one noisy vendor must never bury the rest of the bell. Coalescing — an event whose recipient already holds an unread row with the same kind + subject_ref updates that row: count +1, created_at refreshed, so it re-sorts to the top. Per-source cap — one subject_ref may hold at most 5 unread rows across all kinds; past that, new events coalesce into the source's newest row regardless of kind. Cross-kind / cross-source summaries ("12 claims today") remain the V2 digest feature.
Coalescing · same kind + subject, unread
Claim received ×12
Paper Trail · Corner Shop — 12 claims since you last checked.
Refreshed 3h ago · first at 09:14
Claim #13 arrivescount → ×13 · row re-sorts to top
Row is read, claim #14 arrivesnew unread row appended

Only unread rows coalesce — reading the tally closes it; the next event starts a fresh row. Same rule dedupes timer sweeps (a re-run never writes “about to end” twice).

Per-source cap · N = 5 unread per subject
Corner Shop unread rows5 (claim ×9 · empty · started · ending soon · restocked)
6th event · any kindcoalesces into the newest Corner Shop row
Everyone else’s notificationsuntouched — 45 of 50 slots stay free

Keeps a runaway source from occupying the whole 50-row queue. Schema impact of both mechanisms: one field — count (Nat, default 1).

05Timer events · the cycle-runway watchdogEvent-driven rows fire on activity; the watchdog fires on the passage of time — exactly the case where nobody is looking. A platform sweep monitors every project's cycle runway and writes a warning to the project's admins (fan-out, Decision 2) at each threshold crossing: 30 / 14 / 3 days. Dedupe via subject_ref = project_id + threshold; a threshold re-arms once runway recovers above it, so a project that dips again warns again. Every row deep-links to the project's Billing tab.
Cycle runway · 30 days
Neon District has about a month of cycles left at current burn.
16 Jun
Cycle runway · 14 days
Two weeks left. Top up now to keep Neon District's canisters running.
2 Jul
Cycle runway · 3 days
Critical — Neon District runs dry this week. Top up immediately.
13 Jul

Every watchdog row deep-links to the project's Billing tab. One notification per project per threshold crossing, per admin. Distinct from the event-driven “low balance / top-up performed” rows, which fire on treasury activity.

06Empty statesAll caught up is a personality moment — the Toko character belongs here (empty/success moments, never destructive ones). The panel-scale version stays calm and small; this is not the full-page 920px empty-hero. With the Unread filter active and nothing unread, a one-line variant keeps the read history visible below.

Notifications

You’re all caught up

Nothing waiting. We’ll keep anything that happens here until you’ve seen it.

Notifications

No unread notifications.

Vendor started
Corner Shop went live on the Launchpad.
3d ago
Switch to All to see read items
07Lifecycle, endpoints & IC guaranteesDeveloper reference — not a screen. The write is a best-effort side-effect after the real action commits: icydb has no rollback, so a failed notification write must never fail or reverse the action (the export still happened even if the row didn't land). Personal events write one row; team events fan out one row per project-admin principal (insert_many_atomic, Decision 2). All endpoints are caller-scoped: a principal only ever reads or mutates its own queue.
Row lifecycle
Event commits
Export finishes, vendor sells out, sweep crosses a threshold.
→
Append / coalesce
Row written to the recipient's queue — or an unread same kind + subject row takes count +1.
→
Unread
Badge counts it; the row sits durably across sessions, devices, days.
→
Read
Click-through, per-row, or Mark all read · read_at stamped.
→
Housekeeping
Read rows purge after 7 days; hard cap 50 per principal, oldest read dropped first (Decision 3).
Endpoints · thin wrappers over IcyDB
list_notifications(cursor, unread_only)paginated feed
unread_count()bell badge
mark_read(id) / mark_all_read()read = true
dismiss(id) / clear_read()delete rows
V1 event set (Decision 4)

Upload / generate / export complete · vendor started / empty / ending soon / ended · claim received · sale completed · you were paid · cycles low / top-up · runway watchdog 30/14/3 · listing about to expire · creator access granted · review outcome.

Deferred with their features: auction ended / outbid / won / closing soon (post-v1 auctions), buyback offer (V2), dormancy / at-risk warnings (future permanence doc).