The Loop

Issue 03 · next 1 Oct 2026

Issue 0220 August 2026Still open

The UI is calm. The intelligence is under the hood.

An Android-first personal knowledge corpus. Capture is almost free; synthesis is expensive; the library on the phone and the graph on the server tell different stories. We nearly wiped a live Room database to make a demo green.

Plate 2Save4later · capture to graphfold out Save4later capture and sync path Capture from clipboard, share sheet and media session into a Room library, pushed by WorkManager to a FastAPI worker, enriched into SurrealDB, with a synthesis card returning to the phone.FIG. 2 · CAPTURE → ROOM → GRAPHNOT TO SCALEClipboardzero tapsShare / a11yURL bar · intentMediaSessionplaying nowRoom libraryRoom v4 → v15 · no wipeWorkManagerFastAPI workerenrich · embedsynthesiseSurrealDBgraph + vector“What for you” returns to the phoneFeed said sync enabled. Settings said failed. That line is the product.

Plate 2 · folded out

Save4later capture and sync path Capture from clipboard, share sheet and media session into a Room library, pushed by WorkManager to a FastAPI worker, enriched into SurrealDB, with a synthesis card returning to the phone.FIG. 2 · CAPTURE → ROOM → GRAPHNOT TO SCALEClipboardzero tapsShare / a11yURL bar · intentMediaSessionplaying nowRoom libraryRoom v4 → v15 · no wipeWorkManagerFastAPI workerenrich · embedsynthesiseSurrealDBgraph + vector“What for you” returns to the phoneFeed said sync enabled. Settings said failed. That line is the product.
Detail: the return path, the one that disagrees with the feed · shown at 2×
Subject
Save4later — an Android library where capture is free and synthesis is expensive.
Device
Kotlin · Views/XML · Room · WorkManager · Retrofit · Coil · DataStore
Server
FastAPI · SurrealDB 3 in Docker, with a worker for enrichment
Capture
Clipboard, share intent, URL-bar extraction, MediaSession
Schema
Room v15 · seven migrations in place, including a 4→14 jump. No destructive fallback anywhere
Status
Open — a mature prototype, not on the Play Store
Still open
One sync truth. The feed and Settings still tell different stories

Save4later is not a read-it-later app. We keep having to say that, including to ourselves, because the first drawings looked like Pocket with better manners. The point is not to get through a queue. The point is to save a thing in one motion, let a backend that the user never sees turn it into a node, and then find it again in a year without remembering the title.

The rule we wrote on the wall: the UI is calm; the intelligence is under the hood. No admin panels on the inbox. No block editor. No feature badges. A quiet feed, a detail page that says “what for you,” and a Settings screen for the unglamorous trust work. That is the whole product surface.

It is a mature Android + FastAPI/SurrealDB prototype. It is not on the Play Store. There is no Chrome extension. There is no MCP server we can claim as shipped. Those absences are part of the issue.

The problem

Link rot is the obvious problem and not the real one. The real one is that saving is easy and finding is not. A folder of URLs is a graveyard. A folder of summaries is a slightly more literate graveyard. What we wanted was a library whose next save made the previous ones more useful — categories that are not a tag soup, related items that are not “people also liked,” a synthesis that knows what you have already kept.

That library has to be buildable with one hand, on a phone, while you are in someone else’s article. Clipboard auto-detect. A share sheet. The URL sitting in Chrome’s address bar. YouTube playing in the background. If any of those paths require a form, the save does not happen, and the graph does not grow, and the whole bet collapses.

Then the library has to survive the device. Samsung will kill the process. Room will have a schema. The user will have a real corpus and no patience for “we had to reset the database.”

The constraints

  1. Capture is the product. Clipboard first (zero taps), then share, then accessibility extraction of the URL bar, then a generic tree scan, then an honest failure that tells the user what to do. No “open our in-app browser.”
  2. The feed is the navigation. Today / Yesterday / This Week. Filter chips. Cards with a hero, a source, a title, a short “what for you.” Tap a card, see our detail. Tap an explicit link, go to the original. Mixing those two up is how you eject someone into LinkedIn by accident. We did that. It is in the log.
  3. Never wipe the library. Destructive Room migration is not a fix. It is data loss with a stack trace.
  4. Intelligence stays off the glass. Synthesis, embeddings, graph edges, routing — backend. The phone is a calm client with a local copy of the library.

The stack is the one we actually have: Kotlin, Views/XML, Room, WorkManager, Retrofit, Coil, DataStore on the device; FastAPI and SurrealDB 3 in Docker on the other side, with a worker for enrichment. We evaluated a rewrite in Compose and declined it. Brownfield, on purpose.

What we tried first

Two failures, stacked.

The first was conceptual. We designed a read-later. Queue language leaked into the UI: unread counts, “inbox zero” gravity, a sense that an unsynthesised save was a chore. That is how you build an app people feel guilty about opening. The philosophy doc exists because we had to kill that product without deleting the code. A library you dip into is a different object from a queue you get through. Related items, notes-as-sticky-redirects, “what for you” instead of “summary” — all of that is the repair.

The second was physical, and it is the one that almost cost us the corpus. During a shell rewrite we advertised a Room version lower than the schema already sitting on the Samsung we use as user zero. The app crashed on launch. The tempting fix — the one every Android sample still teaches — is fallbackToDestructiveMigration. Press it and the tests go green. Press it and a corpus that only exists on one phone becomes a clean database.

We did not press it. We restored schema compatibility, wrote an explicit migration, and later, when we found the device was still on schema 4 while the code thought in the teens, we carried a 4→14 path and instrumented it.1 That sentence is longer than the one-liner we wanted to write. It is also the only reason this issue has a library to talk about.

1The corpus
The corpus on the device we use as user zero. Its size is not a metric worth putting in a headline, and it is not a fixture we could regenerate — that is the whole point. It is what we almost deleted with a version number, and it is why every later argument about “just reset the database in development” has to get past that week.

There was a third, smaller humiliation: the feed said sync was enabled; Settings said it had failed. Two screens, one boolean, two truths. Users (we) believed the feed. The worker was not running. Saves piled up locally and the graph on the server forgot they existed.

The turn

Treat the phone as the source of record for the user’s library, and the server as the source of record for intelligence about that library. They will diverge. The product’s job is to make the divergence visible, not to pretend a spinner is a merge.

Capture writes to Room first, always. WorkManager pushes when it can. The backend accepts either a Surreal record id or the Android client_id, because asking the phone to wait for a server id before it can save is how you miss the clipboard window. Enrichment — metadata, embeddings, graph edges, a synthesis pass — happens in a worker, not on the UI thread, not in a blocking “please wait while we think.”

The feed is allowed to show a save that has no synthesis yet. A short local title is honest. A fake “AI is reading this…” theatre is not. When the synthesis comes back it replaces the placeholder in place. If it does not come back, the item is still a saved URL with a date, which is already more than a bookmark.

On the Samsung review we also moved import/export and setup out of the inbox and into Settings. The inbox had become a control panel. The philosophy’s complexity budget is not a slogan; it is a zero-sum rule. Every chip you add to a card is a chip you owe the user a reason for.

The capture fallback chain got the same treatment. YouTube’s accessibility tree lies. Chrome’s URL bar has a view id until it does not. Clipboard permission is a conversation. We built a tiered engine and a short-window duplicate detector so a fumbled share does not create three copies of the same essay. None of that is visible, which is the point.

The artifact

A working capture pipeline on a real Samsung: clipboard, share intent, URL-bar extraction, MediaSession for whatever is playing. A Room library now at v15 that has carried schema 4 through 15 without a wipe. A FastAPI worker that can summarise, categorise, embed, and attach graph edges. A detail surface that is supposed to render “what for you” as markdown without flickering accordion blocks — supposed to, because that rendering is still a defect we filed against ourselves.

FIG. 2 is the path: capture → Room → worker → SurrealDB → back to the phone. The dashed return is the one that lies. When feed and Settings disagree, believe Settings, then go and look at the worker logs. We still have not put a single sync-truth indicator on the card that we trust. Putting a green dot on a card is how you recreate the bug in miniature.

The thing we did not ship, on purpose: a chat. The app is not a place to talk to your library. Notes exist as sticky redirects that change the next synthesis. If you want an agent to query the corpus, that is a later MCP door, and it is not implemented. Printing it as if it were would be the kind of lie this site exists not to tell.

Samsung UAT is where the product actually lives. A capture path that works in an emulator and dies on a One UI overlay is not a path. The accessibility service that reads a URL bar has to survive Chrome’s view-id churn, Brave’s cousin, and YouTube’s habit of stuffing the tree with nodes that look like a title and are a timestamp. We keep a taxonomy of capture mechanisms in the repo because we forgot it once and rebuilt a fallback we already knew was noisy.

The 55-item library on that phone is not a metric we will put in a headline. It is the corpus we almost deleted with a version number. Every later argument about “just reset the database in development” has to get past that week. Development is where the only user is also the only librarian.

What’s still open

Sync truth. One boolean, two screens, still not one story. Until the card, the Settings row, and the worker agree, the graph is a rumour about the library.

Hero composition and source identity. Live review still showed bordered images, text badges (CH / YT / LI) instead of icons, titles that say “Saved from [Source]”, and card taps that sometimes leave the app. We filed that as a remediation milestone, not as a vibe.

Synthesis reliability. Enrichment produces summaries and categories for most items. “Most” is not a claim we would put on a store listing. The worker fails closed more often than a demo would like.

Export lives in a public Downloads folder because the Samsung picker could not see our app-private files. That is the right fix for a one-device life. It is not a backup product.

MCP, Chrome, a second device — future. We will not number them as if they were behind a feature flag.

The calm UI is the hard part. The graph is allowed to be interesting. The glass is not. We are still over budget on chrome, and we know it.

← Index