Nerd stuff
The privacy claim is enforced by architecture, not by policy.
ScribblePost gives children real email on the family's existing mailbox. The headline design decision: there is no ScribblePost server at all. "We don't have your mail" is not a promise — it is a property of the architecture.
All traffic goes straight from the device to the mail provider. The trade-offs are written down honestly in the ADRs: web builds can only serve HTTPS-capable protocols (JMAP, Microsoft Graph) because a browser cannot open a raw IMAP socket — and the team deliberately refused to build a proxy to fake it. Even "server-side" features are emulated as ordinary mail: the parent's notifications are simply emails, sent from the same mailbox.
A single interface (connect, getMessages, sendMessage, …) with four implementations: IMAP/SMTP via pure-Dart enough_mail, JMAP over plain HTTPS, Microsoft Graph with device-code OAuth — the parent types a short code on Microsoft's own page, so the app never sees a password and needs no redirect-URI plumbing — and an in-memory backend that doubles as the onboarding demo mailbox and the test double. Same code, both jobs.
Every incoming message is classified by a stateless domain class into inbox, parent-review or blocked, based on the parent-approved contact list; unknown senders wait in review and a copy is forwarded to the parent — exactly once, tracked locally. Sending is symmetric: every recipient passes canSendTo or the compose throws. House rule: if a PolicyViolation is ever raised at runtime, that is by definition a UI bug — the action should never have been on screen. And HTML mail is never rendered: bodies are reduced to plain text below the domain boundary, so no renderer can be reintroduced by accident.
The account config carries no password. Credentials live in platform secure storage — libsecret on Linux, DPAPI on Windows, Keystore on Android — and the Graph refresh token rotates into the same slot on every connect. The parent PIN is stored as a salted SHA-256 hash with a single-use, 30-minute recovery code.
The drawing model is pure data (a sealed hierarchy of strokes, shapes, 18 stamp kinds and text on a fixed 400×300 canvas), rendered to PNG at 3× for crispness. The paint bucket is a real flood fill: rasterize at 2×, BFS over the RGBA buffer with a 40-per-channel tolerance to cross antialiased crayon edges, then flatten into a base layer. The result travels as a plain scribble.png attachment — grandma's mail client needs nothing special.
Text-to-speech uses flutter_tts everywhere it exists; on Linux, where it does not, the app shells out to speech-dispatcher (spd-say) with the reading rate slowed for young listeners. Profiles (First Steps, Beginner, Advanced) gate features by absence: what a profile does not offer is simply not in the widget tree — nothing is greyed out.