Case study · iOS · on-device LLM · 2026
An iPhone app that answers questions about one Burning Man camp out of ten years of its own group chat, with a three-billion-parameter language model on the phone and no network at all.
Preservation Society is a theme camp: a few hundred people, a decade of know-how, one week a year when all of it matters. Which socket the lag bolts take. Where the water barrels went. The answers exist, buried in 27,000 WhatsApp messages, and the person who knows is asleep or not coming this year.
A chatbot over the chat history, then. Three things make that hard here:
A language model is a guesser of the next word. It has read the internet and none of this chat, and it cannot tell what it knows from what sounds right. So it is not asked to know anything.
Retrieval supplies the facts. The model only phrases them. A name, a date, a number, a place: each has to come from a database row. A claim the model made up is a bug.
General knowledge stays the model's own. She may know the desert is hot. She may not guess which van the water is in.
The expensive thinking happens once, on a Mac, before the event. The phone only searches a catalogue and reads it aloud.
Build time · Mac + cloud, before the burn
↻ cached by content, so a rebuild pays only for messages that changed
Run time · iPhone, offline
↻ every turn is journalled: question, facts given, answer. The journal is the test set.
Enrichment is the step that matters. A large model reads the messages and decides what the camp knows: that a phrase is a thing with a name, that a thread contains a decision, that a joke is a tradition. It writes short dated claims, each stored beside an evidence row pointing at the messages it came from. 1,745 facts, 8,020 evidence rows, and a trigger that refuses a claim without one.
The model was chosen by measuring. On the same corpus, Gemini's strongest model found 149 named things; its fast one found 64. The hundred it missed were the camp's culture, not its inventory.
Embeddings cover what word search misses: "med kit" never matches "first aid". A 300M-parameter encoder turns every claim into 768 numbers, a point in a space where similar meanings sit near each other. The same encoder runs on the phone.
Documents were cut 77%, with a test rather than a rule: a document may go only if every distinctive word in it survives elsewhere. Three sensible folder rules each failed it, one by deleting the only document containing "tourniquet".
Ask "how do we get water delivered?" Stop words go; water delivered stays. Those terms hit five tables at once, scored by match and discounted by age. The question is embedded on the CPU and the two nearest claims above a 0.45 floor join in. What matched becomes the only world the model sees:
FACTS YOU MAY USE: - Water comes by truck on Tuesday and Friday; book it on the shared sheet (2024) [from our manual] About Doris, our vehicle: - carries the water barrels to playa [said 2022-08-19] QUESTION: how do we get water delivered?
Illustrative rows, not the camp's.
The prompt around it is five rules and no examples. It once had examples, and she pasted them back as answers.
Memory is retrieval too. No transcript rides in the prompt. Every turn is written to a second database and comes back like a fact when it matches, labelled as conversation, never as truth. That reaches Tuesday's answer on Friday, which no context window does.
The model is Gemma 4 E2B, 4-bit, through llama.cpp. Phones with less RAM get Gemma 3 1B. The two use different chat markers, and the app finds out which by tokenising a probe, not by reading a filename. For a week the markers were hardcoded wrong and every answer was fluent anyway.
The phone's journal replays. The camp's real questions and their exact fact sheets go through the real model under any prompt, and a scorer checks each answer for a name, number or place that was not in the sheet.
Forty real questions, same facts. With the rules: 2% of answers invent a camp particular, median 15 words. Without them: 82% invent, median 56 words.
That is the trade-off in one line. The rule buys truth and pays in voice; the flat answers people dislike are the price of the 2%. It is also why a fine-tune on the camp's own voice did not ship. It learned the register and invented more, and the number said no.
Each passed its tests. The failure mode in this system is silence.
What ships is decided by whose a fact is, not what it looks like. A campmate's home address never leaves the Mac; the camp's warehouse address must ship, because "where is the warehouse" is a real question. A second, independent scanner reads the final database before every release. It has overruled the builder once, on ten phone numbers a safe-looking refactor exposed.
Same rule on this page: the app's own chrome and the city's public listings only. The camp and its people stay in the dust.
A small FastAPI and Postgres service takes notes whenever a phone finds coverage. The note's id is minted on the phone, so a retry on a dead connection cannot duplicate it. Feed order is assigned under a lock, so a late commit cannot be skipped. Photos are stored by content and never deleted, because two phones photographing the same sign share one file.
One developer, forty testers, no staging in the desert. 199 unit tests, a harness that renders any screen headlessly, resource checks that open the built app rather than the source tree, and unattended TestFlight uploads whose build number is the commit count.
The code is public at github.com/kaymo-ai/Lucy, without the camp's data.
Built with heavy use of Claude Code as a pair.