Ultimate · notification spec

Every ping is a person, showing up

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.

Anatomy — the push on the lock screen

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.

iOS · collapsed
Maya Okonkwonow
Stepped with you by heart: “Trail run before work — up the ridge and back.”
The step photo rides the right slot. iOS keeps the app-icon position for Stepo's mark — the actor lives in the title.
iOS · long-press expanded
Marco Ferri2m
Finished Learn to play guitar — the finale is on air
A Notification Service Extension fetches the photo and attaches it. The finale push is the highest-value tap in the app — it arrives while the Celebrator window is open.
Android · banner
Linh Trầnnow
Stepped with you: “so proud of this one”
Android's largeIcon slot takes the actor's face; the step photo expands below as BigPicture. The person is visible before a single word is read.
Grouped · thread per step
Kofi Asante5m
Stepped with you by share: “Trail run before work…”
Interactions on the same step share a thread — a busy window stacks into one card instead of wallpapering the lock screen. The badge number still counts every unseen row.

The matrix — every notification in the system

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.

NotificationFires whenWho gets itThe pushMediaTap opensStatus
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

One step, one push

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.

1

Achievement wins everything

If the step is final, the finale push goes out and suppresses the rest — it's the one moment worth everyone's lock screen.

2

First step beats new step

Day one of a journey outranks step twelve. Journey followers can't exist yet, so this one goes to the person's followers only.

3

New step is the default

Regular steps ping journey followers and the owner's followers — the everyday heartbeat, at normal priority.

The badge tells the truth

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.

3
A push lands. Badge = server count of rows with no SeenAt.

Arrive SeenAt = null

Each new row is unseen. The Activity tab shows its tangerine dot; the app icon carries the count.

0
Open Activity — everything visible is stamped seen; the badge clears.

Look SeenAt stamps

The "New" section is whatever arrived since your last visit — a warm tint and a dot, cleared by looking, not by tapping.

Tap a row → it's read, and you land on the exact content.

Act ReadAt stamps

Row-level state for per-row indicators. Every mutation returns both counts, so the client reconciles without a second request.

The quiet rules

What keeps the lock screen worthy of trust.

Always enforced

  • Never your own actions. You don't get pinged for hearting your own step or stepping on your own journey.
  • Blocked is silent both ways. No notification crosses a block in either direction; banned and deactivated actors never ping.
  • Private content stays private. If the actor's account is private, the push masks to "@username interacted with your content" — no preview, no photo.
  • Deleted content degrades gracefully. The row stays, struck through; a tap says "This content is no longer available" and falls back to the actor's profile or the feed.
  • Publish-gated. Step pushes wait for media to finish processing — the tap always lands on the real thing.
  • A daily ceiling (100/day) backstops runaway fan-out.

Never sent — it fails the consent test

  • No countdown nags on ordinary steps. We announce that a window opened — an event someone caused. A ticking clock on every step is the app demanding attention. The one earned exception is below.
  • No streak or come-back guilt. "We miss you" is the app cheering for itself — the inversion of everything Stepo is. No streaks exist anywhere; the lock screen doesn't get one.
  • No milestone flattery. "You reached 100 followers" taps through to nothing but your own numbers. An in-app moment at most; never a push.
  • No suggested-content pushes. "Popular on Stepo" is content from strangers you never chose — the attention-economy play we are explicitly not running.
  • No re-pings. One push per step event, ever. Batching (V2) compresses; it never repeats.

Two exceptions to earn back (V2) — they pass the test the bans fail

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.

User controls

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).

All notificationsThe master switch — off means off.
Step-with alertsHearts, comments and shares inside your 24-hour windows.
Social activityFollows, follow requests, acceptances, mentions.
Step engagementHearts, comments and shares after the window, and replies to your comments.
Journey updatesFirst steps, achievements, and new steps in journeys you follow.
Custom follow alertsPer-person and per-journey overrides — V2.
Deltas this board creates (backlog, not silent changes): ① Copy — backend strings say "Step with you by replied:"; this board's voice ("Stepped with you: …", "Replied to you: …", the on-air suffix on first-step and achievement) needs a copy pass in 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.