Case study · 01
Nura
Nura is an AI journaling app, built on background agent jobs. The app itself is calm and almost wordless, but there's a lot going on underneath it. Two of those decisions matter most to me: how I keep the background work from taking itself down when something fails, and how I decide a session can still be trusted. Both are running below.
One stuck job shouldn't freeze the whole app
When a session closes, I kick off two background jobs. One looks back over what happened in the session. The other generates an insight, but only when the user's patterns cross a threshold. I put these on separate queues on purpose. If the analysis job stalls, it can't starve insight generation or block it, and a recovery pass comes back around for anything left stuck. The diagram below runs the real thing.
There are two separate queues here. When the session-analysis worker stalls, the insight-generation queue keeps draining right past it. The failure doesn't cross over. A recovery pass comes back for the stuck job, so nothing quietly disappears. For the user, it means one broken pipeline never freezes the rest of the app.
A stolen session shouldn't stay alive
Access uses a short-lived token. The long-lived one rotates every time it's used. That rotation is what lets me catch theft. If a token that already rotated shows up again, two people are holding it, and one of them shouldn't be. So I revoke every session for that user at once. Killing only the suspicious request would leave the attacker a working token somewhere else. The diagram catches a reuse as it happens.
Every refresh hands back a new token and retires the old one. If an already-rotated token shows up again, that's the fingerprint of a stolen session, and rejecting that one request isn't enough. So I revoke every session for that user at once. For them, it means a leaked token can't quietly hold a door open somewhere else.
The rest of the system, briefly
Provider abstraction
Claude runs as the main model, with OpenRouter as a fallback and a mock version for tests. Switching models means changing one environment variable, and the code stays untouched.
Context assembly
Each user's context comes from their strongest patterns plus their most recent session summaries, all kept under a 4k-token budget. Every request logs what went into it, so cost and behavior stay easy to trace later.
Clean architecture
The domain, application, infrastructure, and presentation layers stay separate, wired together with constructor injection. That keeps every use case testable without spinning up a database or an HTTP server.
Server-driven UI
The app pulls its feature flags, config, and copy from a bootstrap endpoint when it starts. So rolling out a feature or fixing a bit of wording doesn't need an App Store update.