We built a health app with no server.
Me. brings your health data together, lets you add the context only you know, and turns it into one document you can hand to a GP, a coach or a model. It is 290 Swift files and 58,996 lines.
There is no HTTP client in any of them.
- Swift files290
- Lines58,996
- HTTP clients0
Read from the app source, 20 August 2026.
We didn't set out to prove a point about privacy. We set out to avoid running a backend, because there are ten of us and none of us wanted to be woken up by a database. The privacy story turned out to be the easy consequence of that. The rest of it was harder than we expected.
The claim, and how to check it in a minute
To do that job the app holds medical history, medications, blood results and symptoms. The most sensitive category of personal data there is.
None of it reaches us, because there is nowhere for it to go.
You don't have to take that on trust:
grep -r "URLSession\|URLRequest\|dataTask" Me/
Nothing. No networking library either. The only https:// strings anywhere in the codebase are six links that open in Safari: our guides, privacy and terms pages, and App Store links to ChatGPT and Claude for people who want to hand their compiled document to a model.
That is the entire network surface. One analytics SDK ships as a dependency, disclosed in the app with an opt-out, and we will come back to it, because it is the part that bit us.
Most of what you read about privacy is a promise. This is closer to a fact about the binary, and checking it takes less time than reading our privacy policy would.
What you do not build
The list is longer than we expected and it is most of a product:
No accounts. No sign-in, no password reset, no session handling, no account deletion flow, no support tickets from people locked out. No user table. No backend to deploy, monitor, patch or pay for. No database to breach. No server-side receipt validation. No region questions about where health data is stored, because it is stored where the person is standing.
And no privacy policy paragraph explaining what we do with your health data, because we never receive it. That paragraph is usually the hardest one to write honestly. We do not have to write it.
Where the data lives, and the split that matters most
HealthKit is the source. SwiftData on the device is the store. Neither is surprising. The decision worth writing down is that the store is split in two.
SyncPartition divides every model into two lists: 35 user-created types and 5 local-only ones. They get separate ModelConfigurations and separate stores. The user-created store flips between local and CloudKit when someone turns sync on. The local-only store never syncs, whatever the toggle says.
What sits in local-only: Baseline, ManualWorkout, HealthContextProfile, CuratedPrompt, ApiPushLog. Anything derived from HealthKit.
HealthKit data is already on the device and already covered by Apple's own backup. Copying it into a CloudKit container would duplicate the most sensitive data in the app into a second place, to solve a problem that does not exist. Wipe your phone and your HealthKit history comes back on its own. Your check-ins, your notes and your health profile do not, which is exactly why those are the things that sync.
So sync carries what you wrote. It does not carry what your devices measured.
If you build on HealthKit, we'd put this near the top of the list. The default instinct is to sync everything, and everything is the wrong amount.
iCloud is the user's, not ours
When sync is on, it writes to the private database in the person's own iCloud account. We can't read it. We can't enumerate it, back it up, or restore it for them. It is not our sync service with their data in it, it is their storage with their data in it, and the difference shows up the first time someone asks us to recover something and the honest answer is that we can't.
One detail we are glad we built. When someone has opted in but sync cannot actually run, the app says paused rather than on. There is a flag for it, cloudKitFallbackOccurred, and the settings copy changes. A switch that reads on while nothing is syncing is worse than one that admits it is stuck, and health data is the last place to be reassuring about something you have not verified.
Two traps, both expensive, both only visible on a real first launch
The directory that is not there yet. SwiftData's default store URL points into the App Group container, and on a genuinely first launch Library/Application Support does not exist. Core Data quietly recovers from that for a local store. Under CloudKit it does not: you get NSCocoaError(512), "Failed to create file", raised all the way up through try ModelContainer(...). Every first launch with sync enabled fell back to local. The fix is three lines, creating the directory synchronously before any container init, and finding it took considerably longer than that.
The initialiser that traps instead of throwing. CKContainer(identifier:) does not return an error when the iCloud container entitlement is missing. It traps, and there is nothing to catch. So the app now checks the user's sync preference and the entitlement's presence before any CloudKit code runs at all. A do/catch around it is not protection, because the thing you are guarding against never becomes an error.
Neither of these appears on a simulator, on a second launch, or on a device that has run an earlier build.
The hard part: with no server you are close to blind
This is the cost nobody mentions in the local-first pitch.
You can't see what people do. No request logs, no session traces, no funnel, no way to ask the database a question you did not think of in advance. When someone reports that something did not work, you have their sentence and nothing else.
We use TelemetryDeck, disclosed in-app with an opt-out. It is a deliberately thin instrument, and we treated it as thicker than it was.
We had an event whose name was a lie, and no second instrument to catch it.
The event is called notification_sent_checkin. It doesn't fire when a notification is sent. The app books check-in reminders fourteen days ahead and rewrites that whole window every time the app comes to the foreground, and again on every tick of the time picker while someone drags it. The event fires once per rewrite. It counts scheduling passes.
For three months we read it as notifications sent. In August we divided it by the tap count and concluded that our reminders were being ignored 999 times out of 1,000. It went into a written report as the most important thing we had found that week. We came close to redesigning the feature on the strength of it.
It was never an open rate. The numerator counted taps on notifications and the denominator counted people opening the app. The two numbers have nothing to do with each other.
This is worse without a server than with one. With a backend you have a lot of instruments and the wrong ones get contradicted by the right ones. We have about nine events. Nothing cross-checks anything, so a badly named event isn't a small documentation problem, it's just wrong data that nobody can catch.
The rename is queued. We should have named it for where it fires instead of what we meant it to record, which sounds obvious written down.
The boundary that shaped the code
Me. does not interpret your data. It does not score you, grade you, or tell you what anything means. It gathers, structures, and hands over.
A named model may interpret, including inside the app, on the condition that it is named at the point it answers. You always know whose opinion you are reading.
That is a product decision, but it lands in the codebase as a constraint. There is no analysis layer to build, and every time it would be convenient to add a small helpful conclusion, the answer is no. The compile engine gathers. It does not conclude.
What we would do differently
Partition the store on day one. Ours arrived later, as a correction. Deciding what is allowed to leave the device is a much easier conversation before there is a schema to migrate.
Treat a clean first launch as a real test case, not a formality. We found both of the bugs above the way you would expect, which is to say from someone telling us it hadn't worked.
Would we do it again
Yes, and not mainly for the privacy story.
What we didn't anticipate is how much the constraint clarified the product. With no server there is nowhere to put a feature you haven't thought through. Every question turns into a question about what happens on this one device, in this one person's hands. A lot of ideas don't survive being asked that, and on balance that has been good for us, though it didn't feel like it at the time.
Me. is on the App Store. If you build local-first and want to compare notes on the HealthKit side of it, we are easy to find.