¶ case file 5 of 7 · cadence — filed 2023-09 · last verified 2026-08
status: shipped — demo deployment, mock login
↳ consigned at cadence — ¶ 05 · 12:06 · waybill: the committed plan → manifest
Cadence
A calendar and task manager that takes its scheduling in plain English — parsed into structured intent, checked for conflicts, stored in PostgreSQL, covered by a broad automated suite. It is also the file where I found eight of my own IDOR bugs, fixed them, and then wrote the database-level isolation that would make them structurally impossible — and left that half deliberately switched off until a staged cutover. Receipt 05 is where that standing is written down.
[ problem ] · § as found
Planning splinters across tools — notes here, reminders there, scheduling language nowhere — and nobody notices two meetings colliding until they collide.
constraints —
- Support natural language input for scheduling.
- Keep full-stack behavior tested across frontend, backend, and integration layers.
- Use PostgreSQL and indexed queries for calendar data.
- Keep the workspace usable across multiple planning views.
“lunch with Sam1 next Tuesday at noon2, add a Meet3.”
- 1 who
- sam
- 2 when
- tue · 12:00
- 3 meet
- link attached
fig. 1 — the parse, filed: one sentence, one committed event. the numerals are the paper’s own sidenote hand.
a drawn plate, at rest — not a screenshot. the sentence is illustrative, never user data; the parse path it depicts — chrono-node and compromise into a structured event — is the app’s real one.
[ architecture ] · § fig. 2, inked
A React and TypeScript interface sends scheduling workflows through a full-stack app backed by PostgreSQL, with the suite covering frontend, backend and integration paths.
React 19
Task and calendar workspace
natural language
NLP input
chrono-node and compromise
regression coverage
1,186 tests
Frontend, backend, integration
parsed intent
App services
Scheduling and conflict logic
every read, scoped to its owner
Owner-scoped checks
JWT identity; rls enforced on 7 tables, FORCE, 22 policies
calendar records
PostgreSQL
Indexed calendar data
[ decisions ] · § as filed
d1 — use nlp for smart input · accepted
Scheduling should accept natural language instead of only rigid forms.1
1 tradeoff — Parsing ambiguity requires validation and clear fallbacks.
d2 — build broad automated tests · accepted
Calendar behavior has many edge cases and regressions are expensive.2
2 tradeoff — More test maintenance, but higher confidence in app behavior.
d3 — carry the user id to postgres in a transaction-local guc, not a per-user connection · accepted
Cadence shares a Supabase pooler, so a session-level SET can be handed to whichever tenant borrows that connection next. An AsyncLocalStorage store carries the id the auth middleware already verified, and every statement runs inside a transaction that first sets app.user_id with set_config(..., true) — the third argument is what makes the setting die with the transaction.3
3 tradeoff — Every read costs a transaction, and any query path that goes around the wrapper silently loses its scope — which is why the isolation suite drives the real query() and withTransaction() instead of raw SQL, and why one of its tests exists only to prove the GUC does not survive on a reused pool.
d4 — ship the rls migration inert and cut over by hand · accepted
Turning FORCE ROW LEVEL SECURITY on before every request reliably sets the GUC locks the app out of its own data. The migration states the order in its own header — the GUC wiring deploys first, the policies are applied after — and nothing in the app auto-applies the file.4
4 tradeoff — For eleven days the database enforced nothing: shipping the migration inert meant the isolation was the application’s discipline and not Postgres’s, and the boundary rows said so out loud. That window closed on 2026-08-03, when 0002 was run against production by hand — the cutover this decision deliberately deferred. The cost that remains is the one the choice actually bought: there is a hand step between “written” and “enforcing”, it is invisible to CI, and nothing but this file records that it was taken.
[ validation ] · § the receipts
validation
walks the receipts below, top to bottom, and marks each row this page can verify: a check where a pinned artifact resolves, a ring where the trail ends in an on-page capture, a dash where a claim is described only. walked once, the result settles here and stays.-
01claim: I measured the suite on 2026-08-08: 635 frontend + 551 backend = 1,186 tests passing under vitest, with 0 skipped. On 2026-08-02 it read 635 + 524 = 1,159 with 11 skipped; the 11 were the Postgres row-level-security module, which waited on a database URL no workflow supplied. They now provision their own postgres:16 and run, and the cutover rehearsal added six more.
- artifact: cadence @ abaaea8 ↗
[public]
-
02claim: Scheduling accepts natural language — chrono-node and compromise parse it into structured intent.
- artifact: cadence @ 69a59e7 ↗
[public]
-
03claim: Calendar data sits in PostgreSQL behind indexed queries.
- artifact: cadence @ 69a59e7 ↗
[public]
-
04claim: I found and fixed 8 IDOR vulnerabilities across 9 endpoints. GET and DELETE on tasks, events and tags, and GET on calendars, task-lists and attachments, all looked a record up by id alone — so any signed-in user could read or delete another user’s records by guessing one. Every lookup is now scoped to the caller and a miss returns 404, not 403: an id that is not yours should not be confirmed to exist. The eighth was found three days after the other seven were filed, on tags, and it carried the sharpest finding of the set — the regression test that was supposed to cover it asserted `WHERE id = $1` with the id as its only parameter. It was pinning the vulnerable query in place and reporting green.
-
05claim: The database-level answer to that bug is written, tested, and — since 2026-08-03 — LIVE. 0002_enable_rls.sql puts 22 policies and FORCE ROW LEVEL SECURITY on 7 tenant tables. It sat deployed inert for eight days, because nothing in the app auto-applies it and the cutover was a hand-run step nobody had taken. It has now been run. The app connects as cadence_app — NOSUPERUSER NOBYPASSRLS — so Postgres refuses a cross-user read rather than the handlers refusing it. Verified through the production pooler rather than by reading the migration: a read scoped to the busiest tenant returns exactly its 5 tasks / 2 calendars / 7 tags, an unbound read returns 0 rows, and a cross-tenant INSERT is rejected. The application-level scoping in receipt 04 is still there — it is the second line now, not the only one.
- artifact: 0002_enable_rls.sql @ 54c79e0 ↗
[public]
-
06claim: The per-request identity reaches Postgres without a connection per user: authenticateJWT enters an AsyncLocalStorage store with the id it just verified, and every query runs in a transaction that first executes SELECT set_config('app.user_id', $1, true). The third argument is the whole point — it makes the setting transaction-local, so it cannot ride a pooled connection to the next tenant.
[public]
-
07claim: 11 of 11 isolation tests pass against a real Postgres: a raw unfiltered SELECT as user B returns only B’s rows, an INSERT for someone else fails the WITH CHECK, attachments and task_tags scope through their owning task, and one test exists solely to prove the GUC does not leak across users on a reused pool. They run in CI: the workflow provisions a postgres:16 service and hands the suite RLS_TEST_PG_ADMIN_URL, so the skip guard never fires there — 11 of 11 executed at this pin on 2026-07-24.
[public]
-
08claim: A globally-unique tags table became per-user. name was unique across the whole application, so two people could not both keep an “urgent” tag. The migration backfills each tag’s owner from the tasks it is attached to, clones any tag two users shared so each keeps a private copy, drops the orphans, and swaps the global unique index for (userId, name) with a cascading foreign key.
[public]
-
09claim: A thrown auth error was orphaned in the middleware pipeline. composeMiddleware awaited only each middleware’s own return value, so when a middleware called next() without awaiting it and the downstream authenticateJWT rejected, nothing caught the rejection and no response was ever sent — a request with an expired token hung until the platform timed it out instead of getting its 401. The composer now holds the promise next() starts and awaits it.
-
10claim: Two more holes closed in the same pass: POST and PUT /api/upload accepted uploads with no authentication at all and wrote public-read blobs, and now sit behind the auth middleware chain; and DELETE /api/account cascades one user’s own rows through a single transaction — task tags, attachments, tasks, events, task lists, calendars, profile, then the user.
outcomes
-
11claim: The workspace runs multi-pane planning with a Kanban board and multi-calendar views — inspectable in the live demo.
- artifact: usecadenceapp.vercel.app ↗
[public]
-
12claim: Conflict detection flags overlapping scheduling before it lands.
- artifact: cadence @ 69a59e7 ↗
[public]
ci rows link the public run · repo pins are the exact commits verified 2026-08.
what i’m NOT claiming —
- Re-run 2026-07-31 against HEAD 932625e it is 1,168 passed and 11 skipped (635 frontend + 533 backend) — the tree grew from 87 to 105 test files over 249 commits, and nothing was retracted. 1,145 is my local vitest count from 2026-07 against the pinned commit — not a CI badge, and that commit’s own CI run failed. Main has been green since 2026-07-23, so the caveat is that this number predates the green rather than that the repo is broken.
- No production users or uptime are claimed; the deployment is a demo with a mock-login flow.
- The DB-enforced RLS is now turned on in production, as of 2026-08-03. The app connects as cadence_app — NOSUPERUSER NOBYPASSRLS — and seven tenant tables carry ENABLE + FORCE with 22 policies. FORCE matters: without it, policies do not apply to the table owner, and the owner is what an application usually connects as. Verified through the production pooler rather than by reading the migration — a read scoped to the busiest tenant returns exactly its 5 tasks / 2 calendars / 7 tags, an unbound read returns 0 rows, and a cross-tenant INSERT is refused. What used to be the application's discipline is now the database's refusal. The cutover cost an hour of broken production and that belongs in the record: moving tag uniqueness to ("userId", name) stranded an inline ON CONFLICT (name) in TaskService — a private copy of an upsert TagService already handled correctly — so every tagged task creation failed while 550 backend tests stayed green, because nothing exercised that copy against a real schema.
- The repo ships the SQL that creates a NOSUPERUSER NOBYPASSRLS role for the app to connect as. It cannot show you which role the production DATABASE_URL actually uses — that is database state, not repository state, and no file here can settle it.
- The hang in receipt 09 was timed once, by hand, against the deployed app, and that number lives in the fix commit’s message and nowhere else — no log, no test, and no timeout setting reproduces it. So this file describes the failure and not its seconds.
[ corrections ] · § the register
erratum · 2026-07-30
The IDOR count was stale at seven. An eighth of exactly the same class — TagService reading and deleting by id alone — was found and fixed on 2026-07-27 at cadence @ 75180a3, three days after the other seven were filed, and this file went on saying seven. It now says eight across nine endpoints, and the eighth carries its own pin rather than moving the pin the first seven were read at: a receipt names the commit its finding was made at, and re-pointing CADENCE_SHA forward would silently restate seven older findings as if they had been re-verified somewhere nobody re-read them. The eighth also brought the sharpest finding in the set, and it is the reason this correction is worth more than the number: the regression test that was supposed to cover that route asserted WHERE id = $1 with the id as its only parameter. It was pinning the vulnerable query in place and reporting green. A test can certify a bug. This very page learned the same lesson from the other end on the same day — a Playwright spec here was requiring the page to repeat a false sentence about CI, and retracting the sentence broke the spec that was defending it.
erratum · 2026-07-30
Three statements on this page were false, and every one of them was an UNDER-claim — this file was disowning work it had actually done. It said the 11 isolation tests “do not run in ordinary CI”, that they “are not in CI”, and that “the repo’s own CI is red on main right now”. Checked against the run logs rather than against memory: the workflow at cadence @ 54c79e0 provisions a postgres:16 service and sets RLS_TEST_PG_ADMIN_URL, so the skip guard never fires there, and run 30133037462 records ✓ lib/__tests__/rls.postgres.test.ts (11 tests) 232ms on 2026-07-24. Main has been green since 2026-07-23. The likely origin is a copy: the identical caveat is TRUE of Applied, whose backend workflow really does provision no database, and it appears to have been carried across to a repo whose CI had since gained one. A false disclaimer is the same broken receipt as a false boast, and arguably the worse one, because a reader has no reason to doubt a claim that costs its author something. What has NOT changed is the standing that matters: the migration is still hand-run, still not applied in production, and receipt 05 still says so.
note · 2026-07-26
The repository was renamed yadava5/taskflow-calendar ⟶ yadava5/cadence, and this file now carries two pins on purpose. The ledger above and receipts 04–10 name cadence @ 54c79e0, the current public head — where the code lives and where the isolation work landed. Receipts 01–03 stay at taskflow-calendar @ 69a59e7, because that is the commit the 1,145-test count was measured at, and re-pinning a number to a commit nobody re-ran it at turns a measurement into a guess. GitHub redirects the old paths; both resolve.
erratum · 2026-07-26
The workspace outcome row linked taskflow-calendar-ashy.vercel.app. That deploy still answers, so nothing 404’d — but it is not the app any more, and the file was printing one host in its meta ledger and another one two sections down. The row now links usecadenceapp.vercel.app, the same host as the rest of the file.
note · 2026-07-30
Receipts 01–03 now read cadence @ 69a59e7 rather than taskflow-calendar @ 69a59e7. The pin is unchanged and that is the whole point of the 2026-07-26 note above: 69a59e7 is still the commit the 1,145-test count was measured at, and nothing has been re-pinned. What changed is only the repository’s name in the label, because the old name now survives on a GitHub redirect and a redirect is not a permalink — it stops resolving the moment any repo called taskflow-calendar exists under this account again. A receipt on a page arguing that every claim terminates at an artifact you can open should not depend on that. The canonical path was fetched and returned 200 before the label was changed.
erratum · 2026-08-02
The note above was not true when it was written. Receipts 01–03, and receipts 11 and 12 with them, went on reading “taskflow-calendar @ 69a59e7” for three more days — the built page rendered the retired name eighteen times — while the register told the reader the labels had already been changed. They read cadence now. This is recorded as an erratum rather than repaired quietly because the error was not a stale name: it was this register claiming a repair it had not made, which is the one failure that costs a corrections register the thing it exists for.
erratum · 2026-08-02
The receipt itself now reads 1,159 — see the erratum below, which corrects this entry's own number. The 2026-07-31 note above already reported that re-run — 635 frontend + 533 backend at 932625e — but only the note was written: the receipt, the architecture summary, the diagram’s own node label and the /evidence index all went on saying 1,145 for another two days, so the page argued with itself and the reader had to find the erratum to learn which number was current. Re-measured independently on 2026-08-02 and confirmed identical: 1,168 passing, 11 skipped. The number and its commit move together, so the row is re-pinned to 932625e; receipts 02 and 03 keep 69a59e7, which is still where those source audits were done. This is the second erratum in this register about a correction that was recorded without being applied, and the pair is why the audit now ends by widening the drift gate instead of only fixing the values.
erratum · 2026-08-02
1,168 became 1,159, and the reason is a mistake in the correction above rather than in the code. That entry re-pinned the count to 932625e — which is an UNPUSHED local commit. The source URL 404’d, and the number, though correctly measured, could not be reproduced by anyone: the tree it was taken at does not exist publicly. Re-measured at the public head 8eee84e instead — 635 frontend + 524 backend = 1,159 passing, 11 skipped. The nine-test difference IS 932625e, the fix for nine endpoints that never authenticated; those tests land here the moment that commit is pushed, and the number goes back up on its own. Recorded rather than quietly repaired because the rule this file states — a count and its commit are one fact — has a second half it did not say out loud: the commit has to be one a reader can open.
erratum · 2026-08-07
This is the third drift of the same number, and this time it shipped as three different values at once. The architecture figure’s node label read 1,159 — twice on the built page — the receipt beside it read 1,185, and the /evidence index read 1,179. Every gate was green, and the erratum above was itself stale: it says “the receipt itself now reads 1,159” while the receipt had moved to 1,185 and left the figure behind. Re-measured at cadence main dbabc74, CI run 31222343049, all five jobs green: 635 frontend across 58 files + 550 backend across 25 = 1,185 passing, 0 skipped. All three surfaces now read that, and the two pins — this file’s CADENCE_SUITE_SHA and proofManifest’s — moved to dbabc74 with it. The register predicted this: the 2026-08-02 pair ends by saying the audit “widens the drift gate instead of only fixing the values”, and the widening was only half done. check-figures.mjs’s Cadence · suite entry had no manifest: key, so it could not see /evidence at all, and it never bound the node label — the same two holes that entry’s Applied twin had already documented and closed for itself. Both are closed for Cadence now, so the next drift is a red gate rather than a reader’s discovery.
erratum · 2026-08-08
1,185 became 1,186 within a day of the entry above, and the one extra test is worth more than the four surfaces it moved. Exercising the deployed app — signed in as the demo account, against the real API — found GET /api/tags answering 500 for every user: TagService selected t."createdAt" and t."updatedAt" from a table that has five columns and never had those two. git log dates the over-select to the commit that added the service, so tags have been unreachable through the API for the life of the feature. Its 28 unit tests passed throughout, and still pass with the bug deliberately reinstated, because their fixtures are hand-written objects that never reach Postgres — the same shape as the tagged-task failure this register already carries. The fix ships with a regression test that calls the service against the real schema in a real Postgres, which is the +1. Recorded here rather than folded into the entry above because the number the page published yesterday is not the number it publishes today, and a register that quietly restates itself is the failure it exists to prevent.
[ appendix ] · § plates & artifacts
artifact index —
- Source code ↗[repo] · yadava5/cadence @ 69a59e7
- Row-level security migration ↗[repo] · yadava5/cadence @ 54c79e0
