Make a reload of a board that nobody has touched cost a few bytes instead of the whole page.
What it is like today. Measured on main at bff2160, locally: a board page with 40 cards is 55,244 bytes and answers cache-control: private, no-store with no etag. Every reload, every back-navigation, every tab restored after a phone sleeps re-renders and re-ships all of it, however long the board has sat untouched. The number that would make this free is already in the database and already maintained inside every write's transaction: db.boardRevision(boardId).
Done looks like
[ ] A board page carries ETag: W/"<boardRevision>-<viewer>-<build>", where viewer distinguishes what the page shows about identity (anon versus a signed-in id) so one visitor's cached copy can never be reused for another, and build changes when the rendering does — the static asset hashes are a ready-made source for it.
[ ] cache-control becomes private, no-cache (revalidate every time, never a shared cache) with vary: x-forge-identity kept exactly as it is. The pull says why this is no less private than no-store: private keeps it out of every shared cache, no-cache means the browser may hold a copy only to ask about it.
[ ] If-None-Match matching the current ETag answers 304 with no body, the security floor headers and the same vary — through the same helper, so a future response kind cannot forget the floor.
[ ] The ETag covers everything the page depends on: the board's revision, the filter q, and presence if presence is rendered server-side — if presence changes more often than the revision, either fold it into the tag or state in a comment why a slightly stale presence line on a revalidated page is acceptable.
[ ] HEAD keeps behaving exactly like the matching GET, and the 304 path never tries to buffer an event-stream body.
[ ] The landing page gets the same treatment if its inputs are as cheap to name (boardsRevision plus identity); if they are not, it keeps no-store and the pull says why.
[ ] Tests that fail without the change: two identical requests give 200 then 304; a card moved between them gives 200 with a new ETag; the same request under a different identity gives a different ETag and a 200; a 304 carries the CSP and x-content-type-options headers.
[ ] The README's speed section states the rule: an unchanged board revalidates in a round trip with no body.
Files I expect to change
src/routes.ts (the html() helper and the board handler), src/server.ts (the HEAD path, if the 304 shortcut needs it), src/server.test.ts, README.md.
Risks
Waits for #66, which compresses these same responses through the same helper. Compression and validation interact (a strong ETag must differ per encoding; a weak one, as specified here, does not) — landing them in the wrong order means rewriting one of them.
Getting the viewer component wrong is a privacy bug, not a performance one: the test that matters is the same URL under two identities.
Do not cache anything a write can outrun: the revision is bumped inside the write's transaction, so read it in the same request that renders, after the read of the rows.
Suggested: scout, reviewed by mason. Waits for #66.
Make a reload of a board that nobody has touched cost a few bytes instead of the whole page.
What it is like today. Measured on
mainatbff2160, locally: a board page with 40 cards is 55,244 bytes and answerscache-control: private, no-storewith noetag. Every reload, every back-navigation, every tab restored after a phone sleeps re-renders and re-ships all of it, however long the board has sat untouched. The number that would make this free is already in the database and already maintained inside every write's transaction:db.boardRevision(boardId).Done looks like
ETag: W/"<boardRevision>-<viewer>-<build>", whereviewerdistinguishes what the page shows about identity (anon versus a signed-in id) so one visitor's cached copy can never be reused for another, andbuildchanges when the rendering does — the static asset hashes are a ready-made source for it.cache-controlbecomesprivate, no-cache(revalidate every time, never a shared cache) withvary: x-forge-identitykept exactly as it is. The pull says why this is no less private thanno-store:privatekeeps it out of every shared cache,no-cachemeans the browser may hold a copy only to ask about it.If-None-Matchmatching the current ETag answers 304 with no body, the security floor headers and the samevary— through the same helper, so a future response kind cannot forget the floor.q, and presence if presence is rendered server-side — if presence changes more often than the revision, either fold it into the tag or state in a comment why a slightly stale presence line on a revalidated page is acceptable.HEADkeeps behaving exactly like the matchingGET, and the 304 path never tries to buffer an event-stream body.boardsRevisionplus identity); if they are not, it keepsno-storeand the pull says why.x-content-type-optionsheaders.Files I expect to change
src/routes.ts(thehtml()helper and the board handler),src/server.ts(the HEAD path, if the 304 shortcut needs it),src/server.test.ts,README.md.Risks
Suggested: scout, reviewed by mason. Waits for #66.