Owner review 46 — lane E: 알림 as the one inbox (2026-10-19)
Status: Reference · Date: 2026-10-19
Read-only research for owner review 46, mark M14: "Alarm is perhaps the most underworked section of them all. Find best practices and improve the UI/UX of it all, making sure we can aggregate alerts from all parts of the app into here." A migration is cited by number and line (00188:184-226 = supabase/migrations/00188_*.sql, lines 184–226).
Bottom line: a leaky v1 list under a v2 board. 37 of the 101 declared kinds never fire, rows expire in days, grouping is thin, and severity does three jobs (push, band, bell), so FYI news lands in 할 일.
1. Now
- Stream. 101 kinds are declared (packages/app/src/domain/entities/signal.entity.ts:79-218); 64 have an emitter and 37 never fire (8 progression, 3 interclub, 6 subscription,
post_mentionandpost_reaction, thefriend_*kinds …;club_announcement's bridge was dropped in 00514). Emitters callsignals_emit(00188:184-226). The kinds are listed in § 4. - Push. send-push →
signals_claim_pushable(00688:61-106): a category toggle (on, except social, progression and rating; 00107:279-298), quiet hours 22–08 (payments exempt), caps of 10 a day per user and 3 a day per category (00641:117-118; critical kinds bypass them).min_severitydefaults to low, so low kinds (session_created,member_joined) push, against docs/specifications/signal-system.md §5. 24 of the 68 title keys have no push copy and push as the bare category word (supabase/functions/send-push/notification-copy.ts:54-129). - Board against app. The board (flat-v2-list-a.jsx:939-1306): a ⋯ menu, three bands, PersonRow with a dot slot, Choice → a value Sheet, Select mode, swipe with undo. The app (packages/features/home/src/notification-center-screen.tsx): v1 cards, up to 13 chips, a tint and bold for unread, a gear; no select, swipe or faces.
- Read, retention, settings. A row is acknowledged on tap or by 모두 읽음, and dismissed per row with no undo (00107:403). The feed hides expired rows (packages/app/src/adapters/supabase/signal.supabase.ts:86-92), and about half the live kinds expire within 48 hours, so 이전 알림 spans days, not the promised 30 (signal-system.md §7). Settings: 13 push toggles and one quiet-hours window; nothing per kind or per club (packages/features/profile/src/settings-signal-preferences-screen.tsx:195).
- Grouping is thin. The upsert's count++ is never rendered, the row keeps the first actor, and a dismissed row stays dismissed (
post_reply, 00192:120-137; 00188:212). The client folds only read, low, social rows per club and day (domain/rules/signal-resolver.ts:440). No series guard: a 12-week series fans out 12session_createdper member (00239:31-58); a four-round session gives a player about 12 rows. - Bell against 할 일. The bell = the unread RPC minus unread critical and high rows (domain/rules/alert-state.ts:164-170), against the component library (flat-v2-component-library-2026-10-09.md:878: an urgent arrival is one unread news item). So
session_cancelled(no expiry, no resolver) skips the bell and sits in 할 일 until dismissed; DMs (chat is high, 00416:110) skip it too; the 안 읽음 chip uses the un-netted count.useHomeTasks(domain/rules/home-tasks.rules.ts:23) has no screen, while the library (:1565) wants 홈's 전체 보기 to open 알림's 할 일 band, which today is severity plus admin counters. - No signal where there should be one (absence checked): a friend request or acceptance (00118), a club invite (00686), a score correction (00681), the host told that someone joined, a seat released when a member leaves (00322, 00698), mentions, reactions, replies to a comment, an officer's 공지 (the same
post_new, 00525:122), achievements (00021), 대항전 (00098), a member leaving → the officials (00410:47 goes to the leaver); a cancellation skips the waitlist (00672:137).
2. References (read in full unless "snippet")
- Events, tasks and chat. Linear's Inbox is events only (Priority/Other, snooze): https://linear.app/docs/inbox . GitHub's inbox doubles as a queue (Done/Saved, 3-month retention, reason filters, grouping by date): https://docs.github.com/en/subscriptions-and-notifications/how-tos/viewing-and-triaging-notifications/managing-notifications-from-your-inbox . Slack's Activity (January 2026): All and DMs by default, other filters opt-in, mark-as-read keeps the row, clear hides it, swipe left, multi-select, DMs now included: https://slack.com/intl/ko-kr/help/articles/46751260742035 .
- Aggregation. "Julie and 14 others", keyed by verb and day; the Facebook model, where opening the bell marks seen and a tap marks read (snippet): https://getstream.io/activity-feeds/docs/javascript/v2/flat-feeds/ . Android: a summary plus children that stand alone: https://developer.android.com/develop/ui/views/notifications/group . NN/g: bundle bursts, ask permission after value, settings in the app: https://www.nngroup.com/articles/push-notification/ .
- Priority and digest. Apple's four levels, Time Sensitive only for "now or within the hour", Scheduled Summary: https://developer.apple.com/design/human-interface-guidelines/managing-notifications . Android's importance levels, at most three actions, none duplicating the tap, no rating requests: https://developer.android.com/design/ui/mobile/guides/home-screen/notifications . Expo exposes interruptionLevel, threadId, ttl and badge: https://docs.expo.dev/push-notifications/sending-notifications/ .
- Actionable rows. Instagram answers follow requests inside Activity (snippet): https://help.instagram.com/207917546007234 . Ours: AnswerPair is "request lists only" (library :1855).
- Settings. LinkedIn's row menu Delete / Mute / Turn off this type (snippet): https://www.linkedin.com/help/linkedin/answer/a597801 . Strava keeps push apart from an always-on feed, per club all / announcements / none: https://support.strava.com/hc/en-us/articles/216918367-Strava-Notifications . 당근 (2021): 채팅, 키워드, 활동, 기타, each described, plus quiet hours: https://yozm.wishket.com/magazine/detail/342/ . KakaoTalk mutes per room: https://cs.kakao.com/helps_html/1073209644?locale=ko . Toss: push (the OS) against 알림 (the in-app bell), settings per service, functional pushes only: https://developers-apps-in-toss.toss.im/push/intro.html ; its notice of 2026-09-15 (volume may drop if you do not check): https://toss.im/notices/25618 ; third-party: mute a type from the push just received: https://brunch.co.kr/@jyoji/48 .
3. Proposal
It keeps the standing rulings: the bell counts news only, 할 일 lives on 홈, no horizontal scroll, Choice → a value Sheet.
- Structure. Header ‹ 알림 ⋯ (선택, 모두 읽음, 알림 설정). A pinned 할 일 = 홈's 할 일 set in its order (
deriveHomeTasksplus the officer counters), absent when empty. Then time sections 오늘 / 이번 주 / 이전 알림 (30 days), newest first, with no severity sort. A filter Choice on the first head: 전체, 안 읽음, 관리 (officials), 일정, 경기, 회비·입금, 클럽, 사람, 시스템 (the families under § 4; empty ones hidden). [D1] Today reading a row moves it between today's 새 소식 and 이전 bands, and 모두 읽음 empties one; the dot already carries unread (Slack and GitHub keep rows in place). - Row. PersonRow in a notifications context: [dot 8 + 8][40 visual][line 1][line 2][time]. The visual is a face when a person did it (an applicant, a payer, a commenter, a friend), the club's avatar for club and object events, a gray glyph disc for system, trust and rating; no thumbnails. Line 1 is one 해요체 sentence, actor first ("민지님 외 2명이 댓글을 남겼어요"); line 2 is the club, then the one specific thing. Unread = the dot only. An aggregate shows the latest actor and 외 N and opens the object; request groups fold in place (KIND-GROUPS).
- Actions. A tap opens. An inline AnswerPair only for a pending 친구 요청 [D3: library :1855 says request lists only]; money, 가입 심사 and scores open their screen. Swipe and Select = 삭제 with 8 seconds of 되돌리기; a dismissed aggregate returns on a new event. The row menu's "이런 알림 끄기" = that family's push toggle.
- Bell and 할 일. The bell = the unread rows here (one per group), excluding 할 일 and chat, and equal to the 안 읽음 chip; the netting goes [D4]. 할 일 is state, never severity, and never in the bell. Chat stays in 메시지 (its own badge; rows deleted on read, 00687:159), with its push kept [D2; Slack folded DMs in].
- Retention. Visible = created within 30 days and not dismissed;
expires_atonly gates the push and mutes the row ("지난 알림"). - Push. Severity → interruptionLevel (time-sensitive only within the hour:
hold_expiring, a referee transfer, sudden death,match_started), ttl, threadId = the group key, badge = the bell count. [D5] The spec says low never pushes and the SQL pushes it; a per-club level 전체 / 공지만 / 끔 (Strava) is preferred to raisingmin_severity. Later: a daily digest for what the caps drop. - Settings. ⋯ → 알림 설정: the 13 categories regrouped into the six families, one line each; quiet hours and the payment exemption kept; the Android channels stay 13 until a native build.
4. Signal catalog
Push: Y = on by default; n = inbox only (the category is off); ! = critical (bypasses quiet hours and caps); † = no push title (falls back to the category word). Link = getSignalRoute (packages/app/src/presentation/utils/signal-routes.ts:72-163). Group key: now → proposed.
Live
| Kind | Source | To | Push | Group key | Link |
|---|---|---|---|---|---|
session_cancelled !, session_updated | sessions UPDATE trigger, 00672 | confirmed RSVPs (an update also the waitlist) | Y | session × user | clubSession |
starting_soon, reminder_day_before, rsvp_deadline_soon | crons 00443, 00524, 00385 | confirmed RSVPs; members without an RSVP | Y | session × user → user × day | clubSession |
session_created, club_event_created †, club_member_joined, post_new | insert triggers 00239, 00524, 00314, 00525 | the other active members | Y (post_new n) | per item → club × day (a series is one) | clubSession, club, clubPost |
waitlist_promoted, weather_warning, underfilled, late_cancel_alert, player_forfeited †, guest_*_open † | rsvps triggers 00525, 00524; the weather trigger; cron 00444; 00683; 00481–00482 | the member; attendees; the host and admins; guests | Y | session | clubSession |
matchup_published, match_started †, match_completed | match triggers 00525, 00385 | players | Y | match → session recap | matchBoard |
score_pending †, score_disputed, sudden_death †, referee_transfer † | score-call RPCs 00240–00242, 00402 | participants | Y | match | matchBoard |
elo_changed, tier_promoted, tier_demoted, outcome_unrated † | apply_match_ratings, 00682 | players | n | match → session recap | records, scorecard |
the seven dues_* kinds (severely_overdue !, waived †) | dues triggers and the tick, 00405, 00524 | the member | Y | dues row | clubDues |
payments: hold_created, hold_expiring, hold_expired, confirmed, refund_notice; submitted, host_refund_unsettled | session_payments triggers and cron, 00385, 00682, 00684 | the payer; the host and dues managers | Y (no quiet hours) | payment | clubSession; sessionPayments |
join_request_received; join_request_decided; club_application_* † | trigger 00385; RPC 00537; 00216 | the owner and admins; the applicant; host or applicant | Y | request | joinReview, club |
role_changed, ownership_transferred †, archived †, unarchived †, settings_updated † (dues amount only), member_left † | RPCs 00537, 00407, 00410 | the target; both owners; admins or members; the leaver | Y | club | club |
post_reply | comment trigger, 00192 | the post's author | n | post (count unseen) → latest actor + 외 N | clubPost |
no_show, late_cancel_penalty, trust_tier_changed, attendance_* †, manner_tag † | 00639, 00667, 00405, 00416 | the member; admins (flagged → own profile) | Y | session, user | profile |
chat_message_received | notify_dm_recipient, 00416 | thread peers | Y | thread (deleted on read) | dmThread |
venue_correction_* †, owner claim †, contribution † | venue RPCs 00531–00624 | the submitter | Y | correction | venue, claim |
New
| Kind | Source | To | Push | Group key | Link |
|---|---|---|---|---|---|
friend_request_received, friend_request_accepted; club_invite_received | friendships 00118; invite RPC 00686 | the addressee or requester; the invitee | n; Y | pair; invite | profile friends (AnswerPair); join |
rsvp_joined; rsvp_removed; feedback_requested | rsvps; the seat release 00322, 00698; cron | the host; the member; attendees | n; Y; n | session × day | participants; clubSession |
club_notice (공지); post_mention, post_reaction; member_left → officials | club_posts; comments; club_members | members; the author or the mentioned; officials | Y; n; n | post; post × day; club × day | clubPost; members |
score_corrected; session_recap; achievement_unlocked | 00681; session completed; 00021 | players; attendees; the member | Y; n; n | match; session; achievement | scorecard; records; profile |
interclub_challenge_* | inter_club_challenges, 00098 | both clubs' admins | Y | challenge | club |
Families: 일정 = session + rsvp; 경기 = matchup + rating + progression; 회비·입금 = dues + payment; 클럽 = club + interclub + club posts; 사람 = comments, mentions, friends, trust; 시스템 = system + venue.
5. Data gaps and migrations
The next migration number is 00699; order 8 → 1, 3 → 2, 4, 5 → 6, 7.
- The feed and count RPCs (00246, 00646; signal.supabase.ts:86): a 30-day window, not
expires_at; chat excluded. signals_emitv2 (00188:184): actor ids and the latest actor; cleardismissed_aton aggregate kinds.signals_dismiss_batchandsignals_undismiss.- Re-key the emitters:
session_created00239,post_new00525,member_joined00314, thematch_*kinds andelo_changed00385/00682,rsvp_deadline_soon00385. - New emitters and 24 push titles. send-push sends only to, title, body, data, sound, priority and channelId (supabase/functions/send-push/index.ts:113-127): add ttl, interruptionLevel, threadId and badge (Time Sensitive needs an iOS entitlement, so a native build).
club_members.notify_level(the board's 클럽 알림, library :771, has no column).- A
my_tasksRPC if 알림 rows need object ids (get_home_signalsreturns counts only, 00694:97-144). - Client only: the bell without netting, the 할 일 band from
deriveHomeTasks, the family map, andgetOpenObligationCountsdropped.
Decisions needed: D1 time sections or read bands; D2 chat in 알림; D3 an inline AnswerPair; D4 urgent arrivals on the bell; D5 low-severity push or a per-club level.
Not verified
No 2025–26 inbox write-up was found for Toss, 당근 (2021) or Kakao; no Material 3 notification page was found (the Android design guide is used). The Instagram, LinkedIn and Facebook items are search snippets; Instagram's "님 외 N명" and the time-section labels are unconfirmed. Nothing was run: "never fires" means no emitter in the migrations (variable-typed emits were checked for tier_*, chat and club_application_*). Unverified states: dues submitted by a member, the host-remove flow, report outcomes, the iOS Time Sensitive entitlement.