Engineering
Why We Enforce One Running Timer Across Web and Mobile
One running timer is not a missing feature. It is a constraint we chose, then had to enforce on every device that can start a clock.
Consultants already know the product reason: three open clocks do not make a timesheet. That argument lives in our post on why one running timer beats three open clocks. This post is the engineering one. How do you keep a single billable clock honest when the same workspace is open in a laptop tab, an iPhone Live Activity, and an Android notification — and Firebase is syncing all of them?
The rule we shipped: a workspace has one running timer. Starting a task stops whatever was already running. The sidebar, the browser tab, and the phone all show the same clock — or they show none.
MinuteShark is an Ionic + Angular app wrapped by Capacitor for iOS and Android. Auth, the workspace catalog, and time entries live in Firebase. That stack makes “just start another timer on this device” look easy. It is also how you accidentally bill two clients for the same twenty minutes.
Why “one clock” is a distributed-systems problem
A timer is not a local stopwatch. It is a document: which task, when it started, who started it, and whether it is still open. The moment two devices can write that document, you have a race.
The races we refused to treat as “eventual consistency, it will settle”:
- Laptop start, phone start. You tap Play on Homepage mockups in the browser, then say “start a timer on wireframes” from the car. Two starts, one human, one overlapping interval.
- Stop on one device, start on another before sync lands. The phone still thinks the old timer is running. A naive client would stack a second open interval.
- AI start while a sidebar clock is live. MinuteShark AI can propose “Start a timer on the wireframes task.” If we applied that without looking at the active timer, the assistant would become a second clock.
Firestore will happily store two open intervals if the client asks it to. The product invariant has to live in the write path, not in a hope that the UI only shows one Play button.
The write path: stop-then-start, not append
Every start — dashboard search, project task list, to-do widget, active-timer sidebar, or a confirmed AI card — goes through the same idea: if a timer is already running for this user in this workspace, close it, then open the new one.
What “one running timer” means in the data
- At most one open interval per user in a workspace. A second start is a stop + start, not a second row that stays open.
- The sidebar is a view, not a second clock. Elapsed time, start time, task / project / client, and the task total all read the same open interval.
- Confirm still applies. If the start comes from MinuteShark AI, nothing is written until you confirm the proposal card. The card should refuse a silent second timer, not hide the conflict.
We would rather you see the switch (“you were on Homepage; you are now on wireframes”) than discover two overlapping lines in History on Friday.
Firebase Functions sit on the mutations that need to be true even if a stale Capacitor webview retries. The Angular client can be optimistic for feel — the sidebar should move immediately — but the source of truth is the committed interval, not the last tap that has not synced.
What each surface is allowed to show
Once the data model is singular, the surfaces have to stay boring on purpose.
Web: sidebar + document title
The Ionic web app is keyboard-first. You start from search more often than from a project tree. The running timer still has to be visible when you have already tabbed into Figma or a terminal. The document title can show running time and the task name so the tab itself is a clock. That ambient display is a read of the same interval — we wrote about the tab-title product choice separately. Engineering’s job is to make sure the title never advertises a timer Firestore already closed.
iOS: Live Activities are not a second start button
Capacitor 7 lets us keep one Angular codebase and still ship a native Live Activity on the Dynamic Island and Lock Screen. That surface is a status channel: elapsed time, the task you are on. It is not a place to spawn a second interval because the phone felt “local.” Local notifications and Live Activities subscribe to the same open timer the web sidebar uses.
Android: the notification is a mirror
Same rule. A persistent notification that you can tap back into the app is useful. A notification that starts a private clock the laptop cannot see is how you invent two truths. Offline on mobile is a real benefit — you can keep working when the cafe Wi-Fi dies — but reconnecting cannot append a second open interval next to the one the laptop already stopped.
Why we did not ship simultaneous timers
The request comes up. “I was on a client call and also reviewing a PR.” Split attention is real. Split clocks are a lie about how billing works for a solo consultant.
If we stored two open intervals, every report, every rounded BILLABLE total, and every invoice line would have to decide whether overlapping minutes are two billable hours or one. That is an accounting product, not a time tracker. We already keep original duration and optional rounding-up as a workspace rule. We do not also need a policy engine for “50% of this overlap belongs to Contoso.”
The honest workflow is sequential: stop the call task, start the review task, or log the forgotten slice later from History or from chat. MinuteShark AI can propose “I forgot to log 30 minutes on yesterday’s standup” — you still confirm the card. That is how forgotten time stays a human edit, not a second ghost clock.
What we evaluate when a start request arrives
The interesting bugs are not “Play does not start.” They are “Play started something that already existed somewhere else.”
- Does a start from Algolia search stop the sidebar timer, or stack?
- Does a confirmed AI start refuse to open a second interval if one is already running?
- Does a locked or closed task refuse a start, even from mobile?
- After a phone reconnects, do we merge into the surviving interval instead of creating a twin?
Those checks are product tests that happen to use Firestore and Capacitor. They are not a feature list for simultaneous clocks.
What this is not
This is not idle detection. We do not pause you because the laptop lid closed. We do not invent time from screenshots. One running timer is a write invariant, not surveillance. If you forget to stop, you edit the start or end from the sidebar clock or from History. The original duration stays; rounding is a separate, optional billing rule.
It is also not an enterprise “who is online” dashboard. Primary audience is still a freelancer or a tiny consulting team. The enforcement exists so your day has one story when you open Reports on Friday.
If you are setting up the hierarchy for the first time — workspace, client, project, task — start from the getting started guide. Time still belongs on a task. The clock just refuses to belong to two tasks at once.