Skip to content
C.W.K.
Stream
Lesson 05 of 05 · published

The Doors That Need No Screen

~12 min · native-capture, ios, watchos, share-sheet, spool, intents

Level 0Cold Hearth
0 XP0/35 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The shortest path to a captured thought is the one that never opens the app."

Three Things a Browser Can't Have

The last four lessons taught the capture contract on the web: no required fields, a local-first outbox, the composer trio, two-truth location. That contract lives in a PWA, and the PWA stays — it is the zero-install surface on every device that can open a browser. But three capture wants are OS-level, and no tailnet PWA can have them: a share sheet that takes whatever another app is holding, the camera as a capture surface rather than a file picker, and storage iOS will not evict — a queue the system can't reclaim under memory pressure, which is what "device storage is a staging area, not the final archive" was always waiting for. So Vesta grew a second client: a native iOS/iPadOS app, and later an Apple Watch. A second client of the same engine — never a second store.

A new door is a new entrance to the same room, not a new room. Every committing surface — the browser composer, the share sheet, the intent, the watch — commits to the same engine, under the same two date truths, into the same append-only log. The test a new door has to pass is not which features it can show off; it is which invariants it must re-earn: local-first save, capture truth over arrival truth, foreground-only location. A door that can't keep the contract doesn't get built.

The Doors

Three doors now commit crumbs without ever opening the app, and a fourth points at the one that does. The share sheet commits a crumb from inside any other app — the second capture channel the Day One adoption notes had deferred for want of a clean one, settled by an extension instead of a chat-app hop. What arrived is saved; what did not is named — if one attachment provider never calls back, the sheet saves the rest and tells you exactly which piece was missed instead of hanging forever. The intent lets Siri, Shortcuts, and the Action Button commit a dictated fragment straight into the queue without opening anything — shorter than the composer, which makes it the shortest path the platform allows to Vesta's own five-second sentence. And the Apple Watch is capture and nothing else: no stream, no calendar, no journal picker. A wrist is a five-second surface even more than a phone is, and every control that isn't capture stands between you and the thought. The widget is the fourth door, and it does not capture — it shows today's count and anything still waiting on the device, and its tap target opens the composer. That is the honest promise a widget can keep: iOS gives it seconds, no keyboard, and no network guarantee, so it reads the shared app container and never dials the engine. A tile that phoned the office would be stale, blank, or spinning — and the real no-screen capture lives in the intent.

Two details show how deep the discipline runs. On the watch, dictation opens directly — WatchKit's input controller takes a suggestions parameter where nil is load-bearing: an empty array means "a list with nothing in it" and shows the list screen, while only nil means "there is no list" and opens the microphone. And the complication — the little face widget — is a button that shows no state: a watch face is read at a glance, and its timeline refreshes on the OS's schedule rather than on capture, so a count printed there would be confidently stale within minutes. A stale number on the one surface you never open is worse than no number.

The Spool: a Deliberate Divergence

The web outbox from the save-path lesson is one JSON document, and the kit owns its state machine. The phone couldn't take that shape, for a reason that only exists natively: it has several writers — the app, the share extension, the intent, and whatever comes next — and a shared read-modify-write document can lose one writer's commit to a race. So the native queue, the spool, is a directory per crumb with the manifest written last: writers only ever create files, only the app rewrites, no lock and no race. And the item's id is the client_request_id — the engine's dedupe key and the local identity are one and the same, so an interrupted send is always safe to repeat. This is the final track's discipline in miniature: inherit the contract (local-first, drain when the engine returns), diverge where the domain truly differs (many writers), and write the divergence down in the architecture doc with its reason.

The Watch Keeps Its Own Truth

The wrist lane holds this quest's cleanest proof that the two-date-truths rule is load-bearing. The watch is not on the tailnet and never will be — there is no engine it can reach, so the phone is its engine. A spoken crumb commits to the watch's own queue first, then rides the system's transfer queue — delivered in order, surviving a phone that's off or out of range — and the entry is dropped only once the transfer reports it landed. Here is the beat: when the phone finally commits the crumb, it stamps it with the watch's capture truth, not the moment of arrival. The handoff can take hours, and a crumb claiming it was written when it arrived would land on the wrong day. captured_at is device truth, and the device that matters is the one you spoke into.

When the Engine Says No

One more honest-failure beat landed with the native doors. A crumb the engine rejects as invalid — a real 4xx — no longer holds the queue behind it: it steps aside into a needs-attention quarantine while the rest of the day keeps draining, because the outbox exists to defeat flaky networks, not to hold a morning's captures hostage behind one crumb the engine will never accept. Transient answers — 408, 425, 429, and the auth pair 401/403 — still stop the pass, on purpose: a wrong session is not a wrong crumb. The same honesty governs the doors themselves. The native composer first shipped without two of the composer-trio legs, and the architecture doc said so in the trio lesson's own words — recorded as incomplete, not simplified, the invariant named while unmet — until the trio was complete. An invariant you can see is an invariant you can trust.

Code

Four writers, one spool, one engine·text
composer (in-app)  --+
share sheet        --+    writers only CREATE:
intent (Siri/etc.) --+--> spool/<id>/media...        a directory per crumb,
watch (via phone)  --+    spool/<id>/manifest.json   manifest written LAST
                                 |
                                 |  only the app rewrites — no lock, no race
                                 v
                      drain when the engine returns
                      item id == client_request_id
                      -> an interrupted send is always safe to repeat

External links

Exercise

Invent a fifth door for Vesta — an email address that files crumbs, a lock-screen voice memo, a button in the car. List which capture invariants your door inherits unchanged (local-first commit, two date truths, no required fields) and find the one place it must diverge deliberately, the way the spool diverged from the outbox. Then write the divergence down the way Vesta's architecture doc does: what changed, and why.
Hint
The honest question is usually about identity and truth, not features: whose captured_at does your door carry, and what does it do when the engine is unreachable? If your door lacks a screen — for a location sample, for a confirmation — the foreground-only rules have already answered for you.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.