Working backwards from the mobile API
I argued for six months about whether one abstraction should be memory, logic and scheduler at once. Building it for a phone settled it in five days.
In August 2025 I threw out most of my own codebase in a few days, by my own count over 80k, and the product still did everything it had done before. The deletion was not a cleanup. It was what happened when I stopped designing the system and started building it for a phone.
Motivation
The project had spent a long stretch on one idea, which was that a document could be a program. You wrote something that looked like a page of notes, the parts of it were blocks, and the blocks carried behavior, so the same object was your memory, your logic, and your schedule at once. That unification was the interesting claim and it was also the thing quietly making everything hard, because a block that has to be all three of those has to satisfy three sets of requirements and you cannot see the conflict from inside the abstraction.
In July I pulled the execution part out into its own package. The seam came out small, and the size of a seam is a testable claim about coupling rather than an opinion about architecture:
fully refactored the idyll engine. it's its own package now. the SaaS part of the project is now decoupled and the bridge code is centralized in less than 400 lines of code, well structured. that bridge code parameterizes the engine and connects it to the components and routes and db etc. so all the hard lifting and semantics and processing is truly in the engine.
If the glue between an engine and the product around it fits in the four hundred lines I counted, which is few enough to read in one sitting, the two really are separable. If it had run to five thousand, the separation would have been a directory layout rather than a boundary.
The same fortnight produced two smaller changes worth keeping. Pusher came out in favour of a plain WebSocket server, which simplified more than it cost, because Pusher solves fan-out at a scale I did not have and charges me a hosted dependency for it. And the database moved from Supabase back onto Convex, two weeks after I had migrated away from Convex, because the thing I actually needed turned out to be its subscription model rather than its storage. Convex pushes query results to clients when the underlying data changes, and once pushing updates out to clients was the primary problem, that stopped being a nice property and started being the reason to be there. Round trips like that are most of what the work is made of, and an account that left them out would be describing a project nobody has ever had.
The mobile constraint
Then the same system got built as a mobile app, and mobile is a constraint that does not negotiate. A phone screen shows one thing. Three jobs cannot share it, and an abstraction that was carrying three jobs has to declare which one it is on any given screen.
The record does not contain a design session where the mobile experience was argued out. It contains the building and the result. The pressure was external and cheap to apply, and the answer arrived in the middle of the work rather than in front of it:
then i thought: "wait why dont we just make documents just documents.. no blocks"
Documents become documents. Not memory and logic and schedule fused into one object, just the memory part, passive, and the other two live somewhere they can be seen.
Splitting documents, prompts and functions
Two weeks later the same instinct ran through the whole codebase:
ur goign to laugh at me again. I did an insane refactor, kind of ditched the entire codebase and just worked backwards from the mobile API. now it's on hono and we're not using vercel anymore, just everything on railway. reduction in code: over 80k. core functionality is all still there.
The stack moved with it. Remix went, Hono came in as the server framework, and the whole thing left Vercel for Railway, which is the ordinary consequence of no longer being a web app that renders pages: once the product is a phone talking to an API, a framework organised around server-rendered routes is carrying weight for a shape you no longer have.
The method is in one clause. I worked backwards from the mobile API, which means the surface the phone actually needed came first and everything upstream was rebuilt to serve it, rather than the server being designed and the phone being given whatever it exposed. Most of what went was not wrong. It was answers to questions the phone never asked.
The conversation block
The replacement abstraction arrived a day earlier and it is the part I would keep if I could keep one thing. The granular approach had been to let a user compose arbitrary automations out of small pieces, and what the phone made obvious is that people were not building automations, they were scripting conversations:
rather than the granular [approach] we created this new block structure that is easy to define in an iOS shortcuts like interface. It's called the conversation block, a start conversation and end conversation. the things that go inside the conversation block are basically on every message after the user response. you can insert operations to manipulate the context so that has access to the history. the insight is that you're not supposed to try to make a granular automation, it's mostly about how to script conversational [flow]
The structure is a start, an end, and a slot between them. What the exact syntax looked like is not in the record, so this is the shape rather than the notation:
start conversation
... blocks here run on every turn,
after the user replies and before the assistant answers
end conversation <- decides, from what those blocks produced,
whether the conversation is over
That middle position is the useful invention, because it is exactly where you want to update a note, fetch something, or change what the assistant can see, and no ordinary chat interface gives you a place to stand there. A refinement the same minute made the closing block conditional rather than fixed:
and the endconversation block can use intermediate results of blocks inside that scope to determine to end or not
So the conversation ends when the work inside it says so, which turns a loop that waits to be stopped into something that knows what it was for. The finished conversation then becomes context for whatever block runs next, which is what made them composable.
What the split cost
The honest line from the same week is the one about what it felt like:
feels bad to kinda nuke 2 months of forcing the unified prompt model
Two months of work on the unified model went into the bin, and the word forcing is doing real work in that sentence, because the model had stopped being a discovery and become something being held in place. What the phone did was make the holding visible.
The document idea did not survive either, and not for the reason you would guess. Looking back in November I described making documents just documents as something we abandoned after another project shipped a similar concept and we realised it was not what we wanted, and the mobile app then became its own thing with a modular approach to declarative agent building underneath it. The simplification was a real gain and it was also a waypoint rather than a destination.
What the constraint settled
A form factor is a cheap way to run an experiment that theory runs badly. Six months of argument about whether one abstraction was carrying too many jobs produced no verdict, and one narrow screen produced a verdict in under a week, because the screen could not be reasoned with. When a design question will not resolve, it is worth asking whether some physical constraint can be brought in to decide it, and whether you are willing to accept the answer if it comes back against the thing you have been holding.