Build notes · 28 July – 24 August 2026
A handwritten-journal companion for iOS. Local-first, model-heavy, and built over twenty-seven days.
The premise
You photograph the page. Twain reads your handwriting, files it under the date you actually wrote it, and, over time, becomes the friend who has read everything you’ve written and remembers it.
One constraint shaped almost every decision that followed:
Your journal stays on your phone.
There is no server database, no account and no sync. There is no remote copy of your writing.
The only server component is a 195-line Cloudflare Worker so beta testers don’t need to provide their own API key. It holds the key and manages usage limits, but journal content passes through without being parsed, stored or logged.
That was a product decision before it was a technical one.
A journal is one of the most private things a person owns. Everything else in Twain’s architecture follows from not wanting to hold it.
By the numbers
| 71 / 17,212 | Swift files / lines |
| 116 | commits across 27 days, built by one person |
| 13 | SwiftData models, all on-device |
| 11 | services behind one observable container |
| 12 + 13 | system prompts and prompt builders, living in one 594-line file |
| 16 | self-healing routines that repair derived state |
| 195 | lines of server code |
| ~2.7¢ | measured cost of one weekly reflection |
| 0 | rows of journal content stored off-device |
Architecture
Twain is built in SwiftUI and SwiftData for iOS 17+.
Pages, digests, notes, corrections, stories and every other model live in a SwiftData store on the phone. There is no remote copy to keep in sync, migrate, reconcile or potentially breach.
The relay exists for one reason: to hold the API key so beta testers don’t have to. It authenticates invite codes, tracks token usage against device-level, daily, monthly and global limits, and streams the request body upstream without parsing it.
The only things it records are a token ID, a timestamp and token counts. There is also a kill switch and individual invite revocation.
The relay knows how much a device has used. It does not know what somebody wrote.
One of the most important architectural decisions was deciding what belongs in deterministic code and what belongs in the model. Twain draws that line at judgement.
Things that should simply be correct happen locally:
NLEmbeddingThey are fast, free, private and deterministic.
The model gets the jobs where judgement actually adds something:
On the record screen, for example, the chart is calculated locally. The paragraph above it is written by Claude from a facts bundle produced by that same local calculation.
Neither should be doing the other’s job. Asking a language model to do arithmetic is a good way to make an app slower, more expensive and less reliable.
Anything Twain generates can be generated again. Reflections, digests, record readings and stories are stored as derived artefacts keyed to a hash of the material they came from.
That means the original journal entry is the source of truth. Everything else is disposable.
This turns out to be enormously useful. It is what allows sixteen different self-healing routines to exist:
Instead of asking the user to repair state, Twain repairs itself when it launches.
Photo to entry
Scanning one page is easy. Scanning years of old notebooks is where things get interesting.
The pipeline is designed around the assumption that people will import batches, the app will occasionally be killed halfway through, the network will fail, pages will be undated and handwriting will sometimes be unreadable. Every step is therefore resumable and idempotent.
Finding the echo
One of Twain’s central experiences is Twain noticed: the small reflection beneath an entry that can sometimes connect today with something written years earlier.
The first problem is retrieval. A vector database would have been the obvious modern answer. It was also unnecessary.
This is one person’s journal. The corpus is small enough to live comfortably in memory, and pure semantic similarity is not actually the signal I care about most.
Instead, each past day receives a score from three things:
That weighting matters. An early version required at least two keyword matches before a day became interesting. That quietly filtered out exactly the kind of connection Twain should notice: the same important person returning across completely different situations.
The final candidate set also deliberately includes two entries outside the normal ranking:
That second one is especially important. Without it, relevance naturally collapses towards the recent past. The system becomes very good at finding something you also wrote about two weeks ago and surprisingly bad at giving a three-year-old thread the chance to surface.
The AI layer
Twain currently has twelve system prompts, each responsible for a distinct job: transcription, combining pages into a day, page continuity, daily reflections, weekly digests, monthly digests, record readings, state readings, kept lines, location extraction, tag narratives and stories.
They all live in one file. That is intentional: I can audit Twain’s entire voice in one sitting.
Most of Twain’s model calls are not conversations. They return typed JSON against a schema. Downstream features then use those fields instead of repeatedly asking the model to reinterpret the same material.
Mood is a good example. Twain has a fixed vocabulary of nineteen emotional states, each with its own glyph, with twenty-four looser synonyms mapped back to those canonical forms.
That means a year can actually be charted consistently. Without a closed vocabulary, the taxonomy would drift every time the model invented a slightly different word.
Anything Twain presents as your words needs to actually be your words. So generated output is checked after the model returns it.
A proposed quote is rejected unless it appears verbatim in the source text after whitespace normalisation. Tags are dropped unless they occur in the entry. If a Story emphasises part of a sentence, that phrase must actually be a substring of the line. Only the words that passed that check get underlined.
This asymmetry is important: the model is allowed to suggest, and deterministic code decides what is allowed through. That is a large part of what makes the output trustworthy.
Some of the hardest prompt work in Twain has been teaching the model to return nothing.
Most journal entries do not contain a profound insight. Most pairs of days are not meaningfully connected. Most pages do not contain a sentence that deserves to be pulled out and preserved.
So several prompts explicitly state that null is not a failure. It is the expected response when there is nothing worth showing.
I tested those prompts adversarially before shipping them. One test paired two stretches of journal whose only common ground was work, a walk and decent weather. Twain returned nothing.
That is the behaviour I want. A weaker system could easily turn that into “You were thinking about work then, and you’re thinking about work again now.” Technically true. Completely useless.
When Twain misreads a word and you fix it, the difference is stored locally as a substitution pair. The same handwriting often produces the same recognition errors, so those corrections can be applied to later transcriptions. The writer’s own vocabulary is also fed forward as context.
There is no fine-tuning and no training pipeline. Just a local dictionary that becomes more useful the longer you use the app.
External services fail. Twain distinguishes between a bad page and a bad service.
If the model endpoint returns a server error or rate limit, the batch stops instead of spending minutes retrying page after page. A background process then probes the service using a single page at widening intervals. When the service comes back, the backlog is released.
Nothing disappears, and the user doesn’t need to repeatedly press Retry.
Home
On the exact anniversary of a journal entry — a month or a year later — Twain can compare that older moment with the period you are living through now. If there is a meaningful connection, a thin bar appears at the top of Home.
Opening it reveals a short sequence: the old date, two or three cards describing what has changed or repeated, and finally the original journal page.
The visual language is deliberately closer to printed ephemera than app furniture: ink, paper, tape, rubber stamps, handwriting ghosting through the sheet. Twain signs the Story in its own hand because it is meant to feel more like a small letter from the past than an analytics feature.
Most days, there is no Story. Then nothing appears.
The latest daily reflection sits beneath the greeting. Tap it and it takes you back to the entry it came from.
The previous fourteen days appear as ink strokes. Writing every day for a full week earns a small crown and a slow light behind the bars.
There is deliberately no numerical streak. Nothing resets. Nothing scolds you for missing a day. It is acknowledgement, not gamification.
Shelf & entry
Correct a transcription and Twain remembers the difference. Those substitutions are applied to future pages, while your own vocabulary becomes additional context for later transcription.
The longer you use it, the less you should need to correct. And that learning remains local.
If the model cannot read a word, Twain marks it as illegible rather than inventing one. Those pages collect in a review list. You provide the missing word, and that correction becomes part of the same local handwriting dictionary.
A small product lesson came from the button for doing this. It originally said Correct. Testers interpreted that as “confirm this transcription is correct”. It now says Edit.
Names become surprisingly difficult once a journal has enough history. If Twain starts seeing evidence that “Max” may refer to more than one person, it asks rather than silently merging them.
You can define them once — for example, “Max (nephew)” with a short line of context. Future extraction then arrives already qualified, and historical entries can be classified against the right person.
“No, that is one person” is also a valid answer and suppresses the ambiguity permanently.
Every time an entry changes, Twain rebuilds its search text and refreshes an on-device semantic embedding. Search then works in two layers: first exact words, then meaning.
So an old page can still surface even when you remember what it was about but not the words you used.
If the current entry genuinely connects with an older one, Twain can surface the thread directly on the page and explain what links them.
If you write a location in the dateline of an entry, Twain can extract it from the text. It geocodes that place once through Apple’s geocoder and caches the result. That is how the atlas is built.
Twain never needs to ask your phone where you are.
Your record
I never wanted Twain to turn a journal into a quantified-self dashboard.
Each day gets one word describing its emotional register from a fixed vocabulary of nineteen states: thriving, tender, restless, tired, rebuilding and others. Each has its own glyph. Those words also carry energy and weight.
There is no 1–10 score, and there won’t be one. The point is to preserve texture while still making change visible over time.
The energy and weight associated with those states let Twain draw the year as one continuous ink line: lighter above, heavier below. Missing periods break the line rather than pretending data exists where it doesn’t. The view can be scrubbed across seven days, thirty, ninety or a year.
An early version of Twain had a “what you return to” panel showing ranked topics. It worked. It also felt completely wrong. It turned a journal into analytics. So I deleted it.
What replaced it are through-lines: two or three things that genuinely keep returning, expressed the way a friend might describe them rather than as a top-ten list.
Twain can suggest lines from your journal that may be worth keeping. You decide whether they enter the collection. Nothing is saved without approval.
If you decline a line, Twain does not keep proposing it. If you set one aside later, it is archived rather than deleted.
When importing old notebooks, candidates accumulate into one review list so you can work through them together instead of hunting through individual historical entries.
Record readings, monthly digests, weekly notes and Stories compose in the background. They can run at launch, when scanning finishes or when you enter a relevant part of the app. Each result is cached against the source material it came from, so unchanged material is not regenerated unnecessarily.
The intent is simple: when you arrive, the reading should already be there.
Reflect
Each completed week can generate its own reading. Every past month containing journal entries is also processed, oldest first, so importing an old notebook gradually builds the historical archive without requiring manual work.
A year of journaling creates fifty-odd weekly reflections. Displaying all of them as one endless list quickly becomes absurd. So years behave more like drawers: one band you can open when you want to look inside.
The monthly reflection reads both the underlying daily material and whichever weekly readings already exist. The two layers therefore build on each other rather than simply repeating the same summary at different lengths.
Stories you choose to keep also live here, tied back to the pages they originally came from and replayable as they first appeared.
Chat & Explore
Chat is retrieval-fed. It is not a general assistant with your entire diary dumped into a context window.
Each conversation carries a longer-term spine built from Twain’s monthly summaries, themselves rolled up from daily summaries, covering up to two years. It also knows whatever you told Twain you are keeping the journal for.
When you ask about a particular day, the context can include that day’s full text, Twain’s previous reflection and any thread already associated with it. So the conversation starts from what Twain has already read rather than pretending every turn is the first.
Explore turns the archive into people, places and topics. But again, the output is not simply a count. Each gets a short written account of how it appears across your journal.
Those narratives are cached against the summaries they came from and only regenerate when the underlying material changes. Open the same person twice without changing anything and there is no reason to pay for the same answer twice.
Because generated artefacts are disposable and tied to their source material, a better model can produce better reflections, digests and readings from the same journal later. No migration. No re-upload.
The handwritten pages are the asset. Everything else is a view that can be rebuilt from them.
Product principles
Twain has a rules file that the product is held to. The central rule is:
Twain is a journal companion — the friend who has read everything — never a data analyst. Raw tag lists, top-N topics, counts, streaks, or anything derivable by counting is never a feature on its own. Generic truisms and fortune-cookie lines are defects, not style.
That rule is not aspirational branding. It has deleted working features.
The ranked-topic view disappeared because it felt like analytics. The streak counter disappeared because it turned journaling into compliance. What remains is a small crown beside a complete week and a soft light behind the rhythm. It never counts, never punishes, and simply does not appear when the week was quiet.
The same applies to Twain’s voice. It should speak about the writer’s life, not analyse their prose. It says “since the winter”, not an ISO date. It does not diagnose. It does not advise. It does not congratulate. It quotes at most once, and only when the writer’s own words are better than anything Twain could add.
Every composed surface was tested against these rules on both rich entries and deliberately thin ones before it was allowed into the app.
The design system is intentionally small. That smallness is part of what made the build move quickly.
There are four colours: paper is the ground, ink is what reads, pen is what acts, and a single warm sepia thread marks connections between entries and nothing else.
I tried adding hue for emphasis and removed it. Against the paper it immediately started to feel cheaper, so interaction became a darker pressure of the same ink instead.
Typography follows a similar rule. One display face handles the shout. A serif belongs to the writer’s own words. A typed face belongs to Twain. A neutral sans belongs to the interface.
The human writes in ink; the machine writes in type.
Dark mode is not simply the palette inverted. The reference is a notebook being read in a lamp-lit bedroom at one in the morning.
Bugs that taught me
Weekly composition originally waited for the scan pipeline to be idle. That sounds sensible until someone has failed pages. Those pages were re-queued every time the app entered the foreground, which meant the “idle” condition could stay false for minutes after launch.
The weekly reflection was also triggered only once. If it missed that window, it never checked again.
Two small implementation details, neither especially alarming in isolation, were enough to make the feature simply not run for the person most likely to need it.
The fix moved weekly composition into its own service with three independent triggers and replaced the global pipeline check with a per-week one.
The first anniversary implementation looked for the nearest written day within ±3 days of subtracting 365 days. Which meant that on the 24th, Twain could show something written on the 27th beneath a very large headline effectively saying “today”.
There was another problem: a year is not always 365 days. Leap years shift the date.
It now uses calendar arithmetic. Exact day, or no Story.
On the model used here, max_tokens covers thinking and the final response together. A monthly reflection with a 2,048-token budget once spent enough of that budget thinking that the actual response simply stopped halfway through a sentence.
No error. No warning. Just half a reflection. Useful thing to know before shipping adaptive thinking into production.
I almost gated weekly reflections behind a paywall because I assumed they would be expensive. Then I measured one. 2,457 tokens in. 581 out. About 2.7¢.
The cost argument largely disappeared. The more interesting product question turned out to be architectural: weekly reflections do not feed much downstream, while monthly digests are used by both chat and daily reflection. The bottleneck wasn’t where I expected it to be.
The transcription-fixing button originally said Correct. Testers read that as an adjective: “This transcription is correct.” I meant it as a verb: “Correct this transcription.” It now says Edit.
Tiny detail, but a useful reminder that ambiguity you cannot see is still ambiguity.
What it adds up to
Building well with language models has turned out to be much more about refusal than I expected.
Ground what they claim. Constrain where they are allowed to speak. Verify what can be verified. Design prompts where returning nothing is often the right answer.
The interesting problem is not getting a model to produce output. That part is easy.
The interesting problem is earning the right to show that output to someone when the subject is their own life.
Everything around that is more familiar product craft: derived data that can be rebuilt, state that repairs itself, a design system small enough to remove hundreds of unnecessary decisions, and product rules strong enough to delete features that technically worked but made Twain feel like the wrong thing.
That is how Twain is built.