The complete notification system: every trigger, who receives it, what
it says, what it shows, and where a tap lands. The doctrine in one line: we announce
people, and we announce openings — never mechanics, never nags. Almost every push
title is a human name; the two exceptions are the app itself (rare announcements) and
a facepile (batching, V2). If a notification wouldn't make sense spoken aloud by that
person, the copy is wrong. In-app surface: notifications.html.
Title = the actor's name. Body = the action + a content preview
(≤150 chars, cut at a word, no emoji). Step and journey pushes carry the step photo
(contentImageUrl, 150px): a compact thumbnail collapsed, the full photo
expanded. Comment-family pushes stay text-only — the words are the content. People
pushes carry the face only.
largeIcon slot takes
the actor's face; the step photo expands below as BigPicture. The person is
visible before a single word is read.15 live types from the backend enum, two proposed recognition types, and the deferred batch pair. Push title is always the actor; the example column shows title + body verbatim. Tap destinations are contract — the row and the push land on the same screen.
| Notification | Fires when | Who gets it | The push | Media | Tap opens | Status |
|---|---|---|---|---|---|---|
| Inside the window · immediate — the whole product happens here | ||||||
| Stepped with · heart | A heart lands inside the step's 24h window | Step owner | Maya OkonkwoStepped with you by heart: Trail run before work — up the ridge and back. |
Face + step photo | Step detail | Live |
| Stepped with · comment | A comment lands inside the window (commenting is stepping with) | Step owner | Linh TrầnStepped with you: so proud of this one |
Face + step photo | Step detail, comments open | Live |
| Stepped with · share | A share inside the window | Step owner | Devin ColeStepped with you by share: Trail run before work… |
Face + step photo | Step detail | Live |
| After the window · normal — late love still counts, quietly | ||||||
| Heart | A heart after the window closed | Step owner | Priya NairHearted your step: First open-water session done. |
Face + step photo | Step detail | Live |
| Comment | A comment after the window closed | Step owner | Jules MoreauCommented: this is the kind of consistency I aspire to |
Face only | Step detail, comments open | Live |
| Share | A share after the window closed | Step owner | Theo SalasShared your step: First open-water session done. |
Face + step photo | Step detail | Live |
| Conversations · high — someone is talking to you, not about you | ||||||
| Reply | A reply to your comment, on anyone's step | Comment author | Hana WatanabeReplied to you: same happened to me week one |
Face only | Step detail, comment highlighted | Live |
| Mention | @username in any comment you're allowed to see | Mentioned user | Ray DelgadoMentioned you: @chaucao you have to try this route |
Face only | Step detail, comment highlighted | Live |
| People · immediate — the relationship events | ||||||
| New follower | Someone follows you (public account) | You | Kofi AsanteStarted following you |
Face only | Their profile · row offers Follow back | Live |
| Follow request | Someone asks to follow you (private account) | You | An NguyễnRequested to follow you |
Face only | Their profile · row holds inline Accept / Decline | Live |
| Request accepted | Your follow request is approved | The requester | Vy ĐặngAccepted your follow request |
Face only | Their profile | Live |
| Journeys · normal — one push per step, chosen by the ladder below | ||||||
| First step | Someone you follow starts a journey (fires at publish, when media finishes) | All their followers | Lena OrtizStarted a journey: Bake sourdough from scratch — day one is on air |
Face + step photo | Step detail | Live |
| Achievement | The final step publishes — the journey is achieved | All their followers + journey followers, deduplicated | Marco FerriFinished Learn to play guitar — the finale is on air |
Face + final-step photo | Journey | Live |
| New step | A regular step lands in a journey you follow | Journey followers (+ owner's followers) | Vy ĐặngNew step in Learning guitar at forty: barre chords finally ring clean |
Face + step photo | Step detail | Live |
| Recognition · high — the emotional peaks; spec + enum addition required | ||||||
| Recognition received | Someone crosses a companion tier on your journey | Journey owner | Priya NairBecame a Die-Hard Fan of your Learn to freedive |
Face · tier chip in-app | Journey, people panel | Proposed |
| Recognition earned | You cross a tier on someone's journey | The supporter (you) | Marco FerriYou're now a Celebrator on Learn to play guitar |
Face · tier chip in-app | Journey | Proposed |
| Stepo · the only non-person voice — used sparingly | ||||||
| Announcement | Platform news worth interrupting for | Everyone, or signed-in users | StepoShare your journey with a link — new in Stepo |
App mark | Whatever it announces | Live |
| Batched · 30-minute window, same action on the same step — schema designed, deferred | ||||||
| Batched step-withs | 2+ people do the same thing to one step within 30 minutes | Step owner | Jules and 6 othersStepped with you by heart |
Facepile (≤4) + step photo | Step detail, interactions list | V2 |
A published step could legally trigger three notifications; it sends exactly one, and never before its media finishes processing — nobody taps into a placeholder. Followers of both the person and the journey are deduplicated.
If the step is final, the finale push goes out and suppresses the rest — it's the one moment worth everyone's lock screen.
Day one of a journey outranks step twelve. Journey followers can't exist yet, so this one goes to the person's followers only.
Regular steps ping journey followers and the owner's followers — the everyday heartbeat, at normal priority.
Instagram's Seen / Read split, server-authoritative: the number on the
icon is always a fresh COUNT of unseen rows, pushed as an absolute value
with every notification — never a client-side +1, so it can't drift.
SeenAt = nullEach new row is unseen. The Activity tab shows its tangerine dot; the app icon carries the count.
SeenAt stampsThe "New" section is whatever arrived since your last visit — a warm tint and a dot, cleared by looking, not by tapping.
ReadAt stampsRow-level state for per-row indicators. Every mutation returns both counts, so the client reconciles without a second request.
What keeps the lock screen worthy of trust.
The metric guarding all of this: notification mute rate. Stepo's core loop — stepping with someone inside 24 hours — travels through this channel, so a muted Stepo is a dead Stepo. Threads can afford milestone and suggested-content pings because it monetizes the attention they harvest; we spend every push from a trust budget the product itself depends on.
The five spec categories plus the master switch — mirrored by
settings-notifications.html. Muting on Stepo filters the feed only; it
does not filter notifications (blocking does).
NotificationJobHandler + the spec table. ② Recognition —
the spec's priority list says "recognition earned" but no recognition type exists
in the table or the enum; add RecognitionReceived /
RecognitionEarned (rows are already mocked in-app). ③ Media — iOS rich
push needs a Notification Service Extension in the Flutter build; Android's
largeIcon needs the actor's avatar URL in the FCM payload (verify the
serialized DTO carries it). ④ Grouping — set a per-step thread-id /
collapse key so one busy window stacks. ⑤ Batching stays V2; the
NotificationBatch schema in the README is the agreed design.