Status: accepted. Implemented 2026-08-24 in kevinvu184/up.
A daily cron walks a real Up bank account over the Up API and writes the only copy of its own state. The runtime is the Val Town free tier, which kills a run at 60 seconds. There is one production SQLite database, no staging, and branches of the val share that same database. A wrong cursor or a stuck lock fails silently, so the design ranks correctness above speed and maintainability above cleverness.
Modules that make decisions never touch sqlite, fetch, or the system clock. Each declares a narrow interface for what it needs (a lock store, a key-value store, now()) and the cron file poller.cron.ts is the composition root, the only place real sqlite, real fetch, and Date.now are wired in. It computes the run deadline once and passes it down. sqliteStore.ts is the single file that imports sqlite, so a storage or schema change touches one file. The correctness-bearing logic (lock expiry, cursor advance, budget cutoff, backoff against deadline) stays small and checkable in isolation.
All state lives in a meta table of key-value pairs, created by a versioned migration runner. Each numbered migration and its version write land in one batch transaction, so a run killed mid-migration leaves either the old schema or the new one, never a half-applied step that a rerun trips over. The runner exists for the second schema change, not the first: with one production database, every later change must be a numbered step that runs exactly once, and the schema_version key makes the schema state readable in the panel and exposes a branch and database mismatch.
The version read treats "no such table: meta" as version 0, because migration 1 creates the table the version lives in. That is the only swallowed error in the codebase. Any other fault on any other statement surfaces, so a real database problem never reads as "already applied".
The lock is a JSON value of expiry and holder token on a row that migration 1 seeds. Acquire and release are each a single conditional UPDATE judged by rowsAffected, so atomicity lives in SQL and two concurrent runs cannot both see the lock as free. A run that loses the race exits without touching the cursor; the poller never waits. The TTL is 5 minutes because it is the real safety mechanism: a run killed at the 60 second wall never executes its release path, and 5 minutes clears the wall while recovering the lock long before the next daily trigger.
The run budgets 45 of the 60 available seconds. The budget is wall-clock and shared with backoff sleeps: the Up client takes the deadline, and if a computed wait (including an honoured Retry-After) would cross it, the client returns an out-of-budget result instead of sleeping, so a 429 with a 30 second Retry-After ends the run cleanly rather than hitting the kill wall. The backfill cursor is persisted after every consumed page, so a killed run resumes with at most the one in-flight page refetched.
Up returns transactions newest first and paginates through opaque prev and next links, so the backfill follows links.next toward the start of history and stores the next-page URL as the cursor. When next is null, the cursor becomes the sentinel done. Every other state key has reset semantics that clear it on the next relevant run, but the sentinel never resets: a cleared cursor would be indistinguishable from "never started" and would restart the backfill from scratch. The exception is commented in the code because it breaks the stated pattern on purpose.
After backfill, the run pages through the last 30 days with whatever budget remains and counts transactions seen. The count is the liveness signal in the panel, proof the token works and the account is reachable, and the seam where a future consumer of stored transactions plugs in. It runs second because backfill is finite and the window repeats daily; a fat window must never starve the backfill.
The storage import is pinned to an explicit version (std/sqlite at v31, the main branch version at build time). The web editor has no lockfile, so the pin is the only version protection. Bumps are deliberate edits, never implicit.
The run migrates before acquiring the lock, deviating from the drafted run shape, because migration 1 creates and seeds the very row the lock's conditional UPDATE targets. The deviation is commented at the call site.
On failure the run writes a last_error key with a timestamp and releases the lock, both best effort: if the fault is sqlite itself, both writes fail, the TTL recovers the lock, and the run logs are the record. Retry waits consume the same budget as page fetches, so a rate-limited run makes less backfill progress rather than risking the kill wall. Branch experiments still run against the live database; the schema version key makes that detectable but not safe. Any future table arrives as migration 2 through the same runner, and any future consumer of transactions plugs into the incremental window without changing the run shape.