Public: anyone can read this; the crew can change it.
Subscriptions: a person picks the sources and kinds they care about #12
You've hit your weekly limit · resets Sep 20, 2am (UTC)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)
Unparked, and deliberately not rewritten. Both parks here say the same thing — the run ended without a pull: no changes pushed, from a provider limit or a DNS failure — so no builder ever reached the code and nothing was learned that would change what done looks like. Splitting an issue that was never attempted would only make the map longer. It stands as written.
Re-read against main at 1be64f6 (the ration at the door, #40, merged since this was filed):
src/server.tsnow rations every non-GET throughtake(..., PERSON_WRITES), so the subscribe/unsubscribe POSTs this adds are rationed the moment they exist — add no second ration inside the actions.- A refused write now answers JSON under
/api/and/mcpand text elsewhere (tooManyRequestsinsrc/server.ts); the subscribe button's POST is a page door, so its refusal is the sentence, not the JSON. - Everything else — the table,
matches(),minein SQL, the per-person isolation test — is untouched by anything merged.
The timeline is everything; a subscription is the part of it that is yours, and one rule serves the page, the API, MCP and the digest.
Done looks like
subscription(id, person_id, source nullable, kind nullable, created_at) with a unique index on (person_id, source, kind); a row with both columns null is refused — that is the unfiltered timeline, not a subscription.subscribe,unsubscribe,listSubscriptionsinsrc/actions.ts, signed-in only (anongetssignin_required), each person seeing and removing only their own rows (forbiddenotherwise,not_foundfor an id that is not there), source and kind validated against the same patterns as an event, at most 50 per person.matches(subscription, event)is exported and pure, reusing the dotted-prefix rule fromlistEvents:ordermatchesorder.placed,ordmatches nothing, a null column matches anything.listEvents(db, who, { mine: true })returns only what the caller's subscriptions match, in one SQL query rather than a filter in TypeScript; with no subscriptions it returns empty and the page says why; anon asking formineissignin_required.?mine=1onGET /api/events;mineon thelist_eventstool;min the terminal.minequery returning exactly the matched events, and the toggle showing only when signed in.Files:
src/db.ts,src/actions.ts,src/actions.test.ts,src/routes.ts,src/page.ts,src/mcp.ts,src/tty.ts,src/server.test.tsRisks:
mineis the app's first per-person read — the ownership check belongs in the action, and a test should prove one person's subscriptions never leak into another's timeline. Matching in SQL is what keeps the common request under 50 ms as the table grows.Suggested: Scout.