---
title: "ChrisGuard: why the phone cannot be the alarm"
description: "A safety timer has to act exactly when its user cannot. That one requirement moved ChrisGuard's deadline from the iPhone to a server, and then reshaped its privacy model and its interface copy."
canonical: https://arnekellmann.de/blog/chrisguard-why-the-phone-cannot-be-the-alarm
published: 2026-10-02
modified: 2026-10-02
lang: en
---

# ChrisGuard: why the phone cannot be the alarm

ChrisGuard is a safety timer for the iPhone. Before a run, a hike or a first date, you set how long it should take. If you do not check in by then, your emergency contact gets a text message. You can choose to include your location. The idea fits into one sentence. Most of the engineering went into a question that sounds simpler: where does the deadline live?

This article follows that question through the code. The answer decided the architecture first. Then it decided what ChrisGuard can honestly say about privacy. Last, it decided what the app may show on its own screen.

<figure>
  <video controls playsinline preload="none" poster="/images/blog/chrisguard-video-poster.webp" width="1280" height="670" aria-label="ChrisGuard: illustrated introduction, 12 seconds, no sound">
    <source src="/videos/blog/chrisguard-introduction.mp4" type="video/mp4" />
    <a href="/videos/blog/chrisguard-introduction.mp4">Watch the ChrisGuard introduction</a>.
  </video>
  <figcaption>This 12-second silent film comes from the house-ad campaign. It uses illustrations, not a recording of the app interface. The SMS bubble does not show the real alert text.</figcaption>
</figure>

## The first design had no server

The first version of ChrisGuard was offline-first and had no backend at all. When a timer ran out, the app prepared a text message with Apple's MessageUI framework. The user then tapped Send. For privacy, this was ideal. No contact, no message and no location ever left the phone.

It also failed in the one case that matters. The person who misses a check-in is often the person who cannot tap anything. Their battery is flat. They have no signal. They are injured, or the phone is at the bottom of a lake. iOS adds a quieter problem: a suspended app does not get to run code at a time of its choosing. A local notification can remind you. It cannot reliably act for you.

A safety timer that needs its user's help at the moment of failure is a reminder app. So in November 2025, ChrisGuard removed MessageUI and moved the emergency text to a server.

## The server holds the deadline, the phone only reports

The backend runs on Cloudflare Workers. The phone tells it three things: a session started, the user extended it, the session ended. The server keeps the deadline. If the deadline passes without an end, the server sends the text. The emergency contact needs no app and no account. A normal SMS reaches them.

That split sounds obvious on paper. The first server-side change still contained the bug that shows why it is not. When the timer ran out, the client marked the session as finished and told the server so. That was the right step in the old design. In the new one, it cancelled the very message the server was about to send. Code review caught it, and a follow-up commit in the same pull request removed the call. It set a rule for everything after it: once the server owns the deadline, the client stops making decisions about it. The phone reports what the user did. It does not decide what happens when the user does nothing.

## Two timers instead of one good one

A server can also miss a moment. ChrisGuard does not try to build one perfect timer. It runs two independent ones and makes it safe for both to fire.

The first is a scheduled alarm. Every user has a Cloudflare Durable Object, a small stateful worker with its own storage. It can set an alarm that wakes it at a given time. When a session starts, the server sets that alarm to the deadline.

The second is a sweep. A scheduled job runs at a short interval and looks for sessions whose deadline lies in the past and which no alarm handled. The commit message names it a safety net, and that is the honest word for it. It exists for the case where the first mechanism fails without an error.

Two mechanisms that can both fire create a new risk: two texts for one emergency, or worse, two half-finished attempts that each think the other one sent. ChrisGuard prevents that in the database. Before anything goes out, a mechanism must claim the session for sending, and only one claim can succeed. Whichever mechanism arrives first sends. The other finds nothing left to do.

Delivery itself retries, but selectively. After a temporary error from the SMS provider, the server tries again a limited number of times. A permanent error, such as an invalid number, fails at once, because a retry would only delay the moment the failure becomes visible. The server records the final result of each send, and the provider can report back whether a message arrived.

None of this makes the system infallible. It reduces the dependence on any one mechanism, and it does so without trading reliability for duplicate alarms.

## The interface may not claim more than the server did

Moving the deadline to the server created a second problem that has nothing to do with servers. The app and the backend now had separate ideas of time, and the app's screen could drift from the truth. In September 2026, a manual QA pass and code review found three such cases.

**The grace period was longer on screen than on the server.** The app showed a short grace period that ended a minute after the planned end. The server sent the text 30 seconds after the planned end. For half a minute, a user could see "you still have time" while the text was already on its way. The fix aligned the app to the server: the warning now starts before the planned end, and the overdue state starts exactly at it.

**The app said "Emergency alert sent" when it could not know.** The overdue card claimed the alert had gone out. But the app gets no delivery report; only the server does. The card now says what the app actually knows. Before the server's send time, it asks the user to confirm they are fine. After it, it says the server sends the text. If the server never confirms the session, it tells the user to contact their people directly.

<figure>
  <a href="/images/blog/chrisguard-states.webp" aria-label="Open full-size screenshot"><img src="/images/blog/chrisguard-states.webp" srcset="/images/blog/chrisguard-states-480w.webp 480w, /images/blog/chrisguard-states-960w.webp 960w, /images/blog/chrisguard-states.webp 1280w" sizes="(max-width: 768px) calc(100vw - 48px), 720px" alt="Two ChrisGuard screens. Left: a running timer with the line “No check-in by 11:11 AM? An SMS goes to Anna – even if your phone is off.” Right: the overdue card “Alert due: The server sends your contacts an SMS. Please get in touch with them.”" width="1280" height="1327" loading="lazy" decoding="async" /></a>
  <figcaption>Left: a running session with the phone-off promise. Right: the overdue card after the fix. App Store screenshots from the app's UI tests with demo data (“Anna” is not a real contact). English interface, September 2026.</figcaption>
</figure>

**The app promised protection before the server agreed.** ChrisGuard's main promise is that the alert goes out even if the phone is off. The app showed that promise as soon as a session started, including sessions the server did not yet accept, for example while offline. Now the phone-off promise appears only for sessions the server confirmed. Until then, the app says the alarm runs only on this iPhone. The same applies to extensions that the user makes offline. They wait in a local queue and sync when the phone can reach the server. While one is waiting, the server still holds the old deadline, so the app drops the promise until the queue is empty.

All three bugs share a cause. Each screen dates from the time when the phone was the source of truth, and each kept speaking as if it still were. In a safety app, a reassuring sentence that the server does not back is not a cosmetic issue. It changes what the user decides to do.

## Privacy after "no servers"

The move to a server had a cost, and ChrisGuard's own copy shows it. Early descriptions said "no servers". Apple's review then asked a direct question: are contacts uploaded? After the move, the answer was yes, and the marketing copy changed to match.

Once data leaves the phone, privacy stops being a property of the architecture. It becomes a list of decisions about what the server keeps, for how long, and in what order it deletes it.

**Minimal payload.** The server receives the contact's phone number and the message text, because it needs both to send. It does not receive contact names. The message text can contain a sender line that the user writes. The server encrypts both fields at rest.

**One position, no history.** If the user turns on location, the server keeps at most one position per session. There is no track. When the deadline passes, the server waits a short moment before it sends, so the phone can upload a fresh position if it still can. The server ignores a position that is several hours old, because it would point the contact to the wrong place.

**Deletion order.** The server deletes the stored position at every end of a session: a normal check-in, an expiry, a failure, an account deletion. The most instructive detail is in the expiry path. The server deletes the position from the database *before* any step that can throw an error, such as decrypting or sending. An earlier version deleted it afterwards. If decryption or the provider failed, the position stayed in the table. The bug did not leak anything to anyone. It only kept data longer than promised, and for a privacy promise that is enough to be wrong. A test against a real database with all migrations now covers this path.

**Deletion as a stop signal.** Deleting the account does more than remove rows. It cancels running sessions, and the sending path checks once more before it sends whether the account still exists. A deletion that arrives seconds before a deadline wins.

The app's own data (sessions, contacts and presets) syncs through the user's private iCloud database. ChrisGuard asks for no name and no e-mail address.

## What the deadline decided

Follow one question through a small product and it explains most of the code. Because the deadline must hold when the user cannot act, it lives on a server, and the phone only reports. Because a server can miss a moment too, two independent timers run, and the database makes sure only one of them sends. Because the server now holds data, privacy becomes a sequence of deletions, and the order matters as much as the deletion itself. And because the phone is no longer the source of truth, every sentence on its screen has to be one the server already backs.
