An AI app with no server, no account, and nothing leaving the phone
A Saturday in the family calendar reads "Piscine 10h avec Tom". That evening, the app hands the child a French dictation about the swim outing with Tom, written at their grade level, working on the agreement rule they got wrong last week.
Nothing about that Saturday leaves the phone. Not the calendar entry, not the child's first name, not their mistakes. There is no account to create, no server to call, no analytics. Put the device in airplane mode and the app behaves exactly as it did a minute earlier.
The app is Dictée Ludique. I built it, it is on the App Store, and it is the clearest answer I have to a question clients now ask me almost every week: can we do the AI part without sending our users' data anywhere?
On Apple hardware the answer is yes, and it ships today. The rest of this is what that took, what it cost me, and what iOS 27 changes in September.
Why not just ask a chatbot
That is the obvious question, and it is the one the whole app came out of.
I did what anyone would do first. I asked a chatbot to write a dictation for a nine-year-old, and it wrote one. A decent one.
Then I did it again the following week and saw what I actually had. A chatbot hands you a text. It does not hand you a system. It does not know that this child was solid on past participle agreement in November and lost it by January, that homophones have never once been tested, or that the last three texts were all about the same holiday. I was carrying all of that myself and retyping it into every prompt. The writing was free. The bookkeeping was the entire job.
What I wanted was the machinery around the writing. Fifteen French spelling skills from the school curriculum, tracked per child, each sitting at mastered, in progress, shaky, or not yet seen. A correction step after every dictation that moves those states, which takes a parent about fifteen seconds. Then a next text aimed at what is shaky first and at what has not come up yet after that, with length and density following the last result, and no settings for anyone to configure.
It also had to feel like a game, which is the other half of the name. Not points and badges. There is deliberately no global score anywhere in the app, because an average across fifteen skills means nothing and would be the one number a child remembered. The game is that the child is handed three cards and picks one, and that the text is about their own Saturday instead of a stranger's. A child writes differently when the dictation is about their birthday rather than the neighbour's cat.
Generation turned out to be the small part. The loop around it is the app, and that is what decided everything below.
Why the cloud was never on the table
There is a regulatory reason, and then there is the reason I actually had.
The regulatory one is ordinary. This is a children's app sold in Europe, so data protection questions arrive with the first parent who opens the privacy screen, not after a funding round. Collecting nothing means there is nothing to declare, nothing to leak, and nothing to explain.
The one that actually decided it: the app reads a family calendar. That is the most intimate file on a phone. It holds hospital appointments, custody handovers, job interviews, the week somebody moved out. I was not going to build a pipe from that into a datacenter I do not own, and no privacy policy I could have written would have made me comfortable with it.
So the constraint came first and the architecture had to live with it.
The calendar gets filtered before the model sees it
This is the part people assume the AI does. It does not.
A raw event title never reaches the model. What reaches the model is a canonical noun phrase produced by a closed list of seventy-three recognised forms. "Piscine 10h avec Tom" comes out as "la sortie piscine". "Anniv de Tom" comes out as "l'anniversaire de Tom", and that first name, matched by a deliberately narrow pattern, is the only free text in the entire pipeline.
Matching happens at word boundaries, which sounds pedantic until you see what it prevents. The keyword for skiing must not fire on Kandinsky. The French abbreviation for a summer camp, colo, must not fire on coloscopie. The longest match wins, so a school outing beats a plain outing.
A second list of sixty-two words disqualifies the whole event on sight: hospital, funeral, divorce, unemployment, prison, and the rest of the registers a family calendar is full of. "Anniversaire de mamie à l'hôpital" is not a usable birthday, even though the word birthday is sitting right there.
Anything the list does not recognise keeps no name at all. It reaches the parent screen as a suggestion with a blank label, and it cannot be used until the parent types a name for it. Refused once, it is never offered again.
The reading window is asymmetric on purpose: two years backwards for one-off events, two weeks forward, and only one week backwards for anything recurring. A child remembers a trip for years. A judo lesson from eighteen months ago is noise.
All of that is ordinary Swift. It is deterministic, it is covered by tests, and it is the part that does the actual privacy work. The model is the last step, not the first.
The model writes the text and nothing else
Generation runs on Apple's on-device model through the Foundation Models framework, which shipped with iOS 26. The output is guided: a @Generable struct holding a short title and an array of sentences, capped at twelve.
Sentences as an array rather than one block of prose, because the model holds a target length far better when it counts the sentences itself.
The model is never asked where the spelling traps are. That belongs to a deterministic analyzer with its own tests. A model that annotates its own output gets it wrong occasionally and nobody notices, which is worse than getting it wrong loudly.
It never sees a template either. The hand-written library has to serve any child, so it carries placeholders and agreement machinery. Here the app already knows the first name and its grammatical gender, so it asks for "Léa est partie" instead of "{ENFANT} est parti{E_ENFANT}". A whole class of errors disappears.
Nothing the model writes gets a free pass
Every generated text goes through the same seven-step validation as the hand-written library: vocabulary graded to the school level, allowed tenses, sentence length, forbidden registers, the skills the child is meant to practise, the child's own first name, and the calendar moment actually being mentioned.
A generated text has no privilege over a text a human reviewed. It has slightly less, because it needed a ceiling the library never did.
When five attempts fail, the app serves a text from the library and says nothing. The child never learns the model had a bad minute.
What on-device actually cost me
This part is not in any WWDC session, so here are the numbers out of my own device logs.
- One generation ran away for 48.9 seconds before dying on "Exceeded model context window size". The four other attempts, same instructions, came back in 2 to 3.5 seconds. The input was never the problem. The output would not stop. That is where the twelve-sentence cap comes from.
- The first draft is consistently about a third of the length asked for. Measured in one session: 29 words, then 184, then 34, against an instruction asking for 55 to 75. Raising the temperature only shakes the dice. Telling the model in plain French what it just got wrong works, so the retry loop now sends back a sentence explaining the miss instead of only nudging the temperature.
- The model produced texts that passed every check and were still wrong for the reader. Eleven sentences for a nine-year-old, word count in range, every sentence under the cap, and the child gets a telegram. A human wrote each template in the library, so that failure mode never existed there. The model does it with real consistency, so the pipeline gained a ceiling on sentence count.
- Apple's guardrails fire without saying why. The error is "Detected content likely to be unsafe" and nothing else. Five retries at rising temperatures with the same inputs fail five times in the same way. What the app does now is take things away in order: the calendar moment first, since it is the only content the app did not write itself, then the content rule in the instructions. The step that unblocks generation is the diagnosis, and it goes into the log as one.
- EventKit cost me an afternoon for reasons that had nothing to do with AI. An EKEventStore created before permission is granted keeps an empty cache for the life of the process, so the calendar read returns zero events even after the parent taps Allow. And authorizationStatus(for:) lags behind the grant: on iPad the request returned true while the status, read thirteen milliseconds later, still said notDetermined. Both look exactly like a permission bug and neither one is.
One more constraint shaped the app more than any of those. Five attempts is the ceiling when a child is watching a "writing your dictation" screen. So the app generates ahead instead. Reading a dictation aloud takes minutes, and the generator works through that window, capped at ten rounds per session, tied to the reading screen and cancelled when it closes. Nothing unvalidated ever enters the cache. It buys time, not leniency.
What iOS 27 changes
Most of the list above is addressed by the version shipping this September, which is an odd thing to be able to write about a framework that is one year old.
- The on-device model has been rebuilt, with better logic and tool calling, and guardrails Apple says produce fewer false positives. That goes straight at my most expensive failure mode. Better instruction following should also cut the retry count, which is the difference between generating ahead of time and generating on demand.
- response.usage now reports input, output, cached and reasoning token counts, model.contextSize exposes the window, and tokenCount(for:) counts a prompt before you send it. My 48.9 second runaway becomes something I can see coming instead of something I find in a log afterwards.
- The on-device model accepts images, and the framework ships an on-device OCRTool backed by Vision. This is the one I keep thinking about. The App Store description currently promises that the app does not photograph the page, because handwriting recognition invents mistakes the child never made, and a spelling app that hallucinates a mistake is worse than no app. Privacy was never the only reason to say no there. But being able to try it entirely on the device, with no upload, is new.
- The LanguageModel protocol lets any model back a session. The app already had that shape: a single front-desk type decides at runtime whether the framework exists on this system, which is how one binary serves both macOS 15 without Apple Intelligence and macOS 26 with it. Now the indirection belongs to the operating system instead of to me.
- DynamicProfile lets one session switch instructions and tools mid-conversation. Writing a dictation and correcting one are two different jobs in this app, and currently two different sessions.
Apple's free cloud model, switched off by default
The same release makes it very easy to stop being on-device.
PrivateCloudComputeLanguageModel gives a 32,000 token context window and a reasoning setting, with no API key, no account, and no cloud bill for small developers under a two million first-time download threshold. Anthropic and Google publish Swift packages that plug into the same session type. Switching this app over is roughly one line, and the text would come back longer, better structured, and far more likely to pass validation on the first attempt.
There is a real case for taking it, and it is not the one I expected when I started. The on-device model writes a good dictation for a nine-year-old. It gets thin when the person asking is an adult learning French, who wants a longer text with subordinate clauses and a wider vocabulary, or a parent who wants something more ambitious than the grade chart suggests. Those readers exist, they already use the app, and the smallest model on the phone is not really writing for them.
So in iOS 27 the app will offer it as a setting, and the setting will ship switched off. Install the app, change nothing, and the promise it makes today still holds: nothing leaves the device, and airplane mode proves it in four seconds.
What makes that option safe to offer at all is the work described further up. Even with the switch on, a raw calendar title still never crosses the network. The whitelist has already reduced it to a canonical noun phrase, so what travels is "la sortie piscine" and a first name, never "RDV Dr Meunier, résultats". The filter was built to protect a child from the model. It turns out to protect the family from the network too.
The privacy screen grows by a sentence, and that is the part I want to get right. One line saying nothing leaves this device becomes two: nothing leaves this device, unless you switch on richer texts, and here is exactly what crosses when you do. Apple's private cloud is a good promise and I believe it. It is still a promise, and a promise belongs behind a switch a parent chose to flip rather than inside a default they never read.
On-device stays the default for an unglamorous reason as well. A dictation on a train, in a car, in a rented house with bad wifi, is exactly when a family reaches for this app. Whatever the setting says, it has to keep working with no network at all, so the on-device engine is not going anywhere.
If you need this in your own app
If you are looking at an app where the data cannot leave the device, and someone has told you that rules out AI, that advice is a year out of date. Health, education, HR, legal, anything involving children, anything where a contract names the country the data has to sit in: the on-device path is real and it works now.
The caveats, since you should hear them from me rather than discover them in month two:
- The on-device model is small. It writes a good short text. It will not reason its way through a long document. Fit the feature to the model rather than the other way around.
- You will write a validation layer. Treat the model as a fast writer with no judgement and put deterministic code around it. That code is where the product actually lives.
- You need a fallback for the device that has no Apple Intelligence at all, and it has to be invisible. Here a hand-written library sits behind the model, and a child cannot tell which one wrote today's text.
- On-device and cloud are not a straight choice between two products. Put the filtering in front of the model rather than inside it, and you can offer the cloud as a setting later without rewriting a word of your privacy screen.
- None of this is web technology. It is EventKit, a privacy manifest, guided generation, and platform behaviour that only shows itself on real hardware, which is one more reason native beats a cross-platform wrapper when the hard part is the operating system.
The whole thing, calendar filter included, took days rather than months. Most of that time went into the validation pipeline and the failure paths, not into the prompt.
Need an app that cannot send data anywhere?
Native iOS and Android, built around on-device AI, EventKit and the platform APIs a webview cannot reach. A working app on your own device by the end of day one, from €500.
Let's talk about my project