A recurring task was the wrong domain model
Routempo started with a familiar product idea: let people define repeatable routines, remind them at the right time, and show their progress. The interface looked like a scheduling problem. The data quickly proved otherwise.
A schedule records intention. It does not tell me whether the person started, completed, skipped, or changed the routine. If I store all of that on one recurring record, every edit risks rewriting history. A timezone change makes the ambiguity worse: did the user mean a fixed instant, or a local morning rhythm that should follow them?
That was the real engineering problem. I needed a model that could preserve what was planned, what actually happened, and the local-time meaning behind both.
Time was not a formatting concern. It was part of the product's domain language.
I separated routine intent from behavioral evidence
I modeled routines as durable plans and sessions as individual opportunities to perform them. Check-ins and reviews belong to the outcome side of the system, not to the recurring definition. That boundary lets a user change tomorrow's plan without changing yesterday's evidence.
The distinction also made the product more honest. A completed checkbox is not the same thing as a reusable routine definition, and a skipped session is still useful behavioral data. PostgreSQL and Drizzle provide queryable, portable history instead of hiding the record in browser-only state.
Once those concepts were separate, the application services became easier to reason about. Planning creates intent. Daily execution records an outcome. Review reads across both without mutating either.
Timezone correctness needed an explicit boundary
The system carries the user's timezone through schedule and completion semantics instead of treating it as a display preference added at the edge. That matters whenever a routine, session, reminder, and calendar event must agree on what “today” means.
I kept the conversion rules behind domain services so route components do not invent their own interpretation of dates. The UI can format a value for a person, while the underlying workflow still has one place that decides which local day and planned session an action belongs to.
This is a small architectural choice with a large failure radius. When temporal rules are duplicated across components, the system can look correct while silently attaching an outcome to the wrong day.
Reminders and calendars could not own the core product
Web Push is a distributed workflow: the browser grants permission, a subscription is stored, and a scheduled server invocation eventually attempts delivery. Google Calendar and Tasks add OAuth scopes, provider configuration, and failure modes outside the planner's control.
I isolated those capabilities behind adapters and kept them optional. A missing provider configuration or rejected notification permission should disable an enhancement, not make the routine planner unusable. Scheduled summaries call into the same domain behavior rather than becoming a second implementation of it.
That boundary also gave failures an owner. The core can record a session successfully even when a notification or external sync needs to be retried or explained separately.
Reliability included an exit path
Because the product stores personal behavioral history, import, export, and deletion are not secondary settings. They are part of the data contract. Users can move their account data in and out, and the system validates those paths alongside the daily workflow.
The final architecture is less about a clever recurrence algorithm and more about stable boundaries: plans versus evidence, local time versus presentation, core behavior versus optional providers, and product data versus user ownership.
That is the senior engineering lesson I took from Routempo: recurring software becomes reliable when the model preserves meaning, not merely timestamps.
