2026 / 01
MakeUp — a relationship OS for two people
OutcomeBuilt one event delivery layer so new features reuse persistence, real-time updates and Web Push instead of reimplementing notification logic each time.
Overview
A full-stack app for any close pair — partners, friends, family — with shared calendars, moods, apologies and nudges, built on an event bus so every feature gets notifications for free.
Context
Every product feature needed to reach the same three notification channels without duplicating delivery code.
System
Problem
Every new feature (moods, wishes, anniversaries, nudges) needed the same three things: persist a notification, push it over SSE to an open tab, and send a Web Push if the app isn't open. Wiring that per-feature would have meant repeating the same plumbing a dozen times.
Investigation
Initial hypothesis
The hidden modal only needed a higher z-index.
A single Kafka topic (makeup-events) carries every domain event. One consumer enriches it with actor info, persists it, fans it out over SSE, and calls the Web Push service — so a brand-new feature only has to call eventPublisher.publish() and the entire notification stack comes for free.
Reminder deduplication
A later addition — a daily job scanning overdue calendar items and anniversaries — reuses the same pipe instead of a separate path. A small reminder-log table keyed on (source, anchor date) deduplicates it, so a repeating "still overdue" reminder and a one-shot anniversary reminder are the same mechanism with different keys.
Evidence
document.elementsFromPoint(x, y)Paint-order inspection showed the modal trapped inside an opacity-created stacking context.
Decision
Keep one event consumer for delivery, and portal the modal directly to document.body.
Result
Also fixed a real production bug this way: a floating bottom-sheet modal was silently rendering underneath the bottom navigation. The cause wasn't a z-index number — a page-transition fade wrapper elsewhere in the tree had an opacity animation that implicitly created its own CSS stacking context, trapping every fixed-position descendant inside it regardless of z-index. Confirmed with document.elementsFromPoint() in a headless browser, fixed by portaling the modal straight to document.body.
What I would change
Current limitation: The reminder model still relies on scheduled scans rather than delayed event delivery.
Next step: Make notification delivery failures observable per channel.
Related notes
An opacity animation was trapping the fixed elements →Stack
Spring Boot · Next.js PWA · PostgreSQL · Kafka · Web Push