Skip to main content

Partnership Duties

A Kvick-internal page (/duties.html) where Tom and James track who owns which duty across the partnership — operational, customer-facing, finance, build, sales, etc. Not a shop-customer-facing feature. Lives in the same Worker because the auth + audit + role gates already exist there.

Live 2026-05-16 (migration 031)

Replaced the per-browser localStorage model with shared D1 state so both partners see the same assignments + change log from any device. Owner-gated end-to-end.

What it tracks

For each "duty" (a predefined item like "deploy production releases" or a custom one added in the UI):

  • Who owns ittom, james, both, or other (free-text classification for non-partner ownership)
  • Outsource recipient — if the duty is delegated, who/what gets it
  • Phase — 1, 2, or 3 (current stage of the partnership build-out)
  • Notes — free-text

Custom items and custom categories can be added through the UI; the change log records every state mutation.

Schema

Four tables, all in migration 031_partnership_duties.sql:

TableRowsPurpose
partnership_duty_stateOne per duty idCurrent assignment / outsource / phase / notes
partnership_custom_itemsUser-added dutiesTitle + category id
partnership_custom_catsUser-added categoriesLabel + sort order
partnership_change_logAppend-onlyEvery state-changing call writes a row (actor + timestamp + what changed)

duty_id is TEXT PRIMARY KEY so the predefined-vs-custom distinction lives in code (predefined ids are namespaced like ops.deploy; custom ids are uuid-prefixed).

Endpoints

All under /api/partnership/*. Every CRUD endpoint requires role_code='owner' — Sys Admin alone is NOT enough; this is a partner-level surface.

EndpointPurpose
GET /api/partnershipFull snapshot: state + custom items + custom cats + change log
PATCH /api/partnership/duty/:duty_idPartial update (any subset of tom/james/other/outsource_to/phase/notes)
POST /api/partnership/custom-itemsAdd a custom duty
POST /api/partnership/custom-categoriesAdd a custom category
POST /api/partnership/logAppend a log entry (the page itself drives this; it's not auto-derived from the PATCH)
DELETE /api/partnership/logClear the log (owner-only; deliberately destructive)
POST /api/partnership/resetWipe all assignments + custom items + cats (owner-only; deliberately destructive)

How the two browsers stay in sync

No WebSocket or SSE; polling is enough at this scale:

  • The duties page polls GET /api/partnership every 5s for cross-browser state
  • Every UI change PATCHes its single duty (so concurrent edits to different duties never conflict)
  • The change log is the single source of timeline truth — both browsers see the same row sequence

If Tom and James edit the same duty within the 5s polling window, last-write-wins on the PATCH. The change log records both attempts; whoever sees the older state on next poll can re-apply.

Why it lives in the Worker (vs a separate Kvick-internal app)

Three reasons:

  • The auth, OAuth allowlist, audit chain, and role-gating infrastructure are already wired here
  • One deploy, one domain, one cookie set — both partners are already signed in via Google OAuth
  • The change log writes to the same audit chain as the rest of the platform, so the timeline is queryable alongside any other forensic question

It's a meta-feature of the platform, not a feature of the shop's product. Filed under runbooks because it's operational tooling for Kvick partners, not bike-shop functionality.

What it is not

  • Not a project tracker. No tasks, no due dates, no dependencies.
  • Not a customer-facing surface. The duties page is reachable only via direct URL; no nav-tab link exists in the operator app.
  • Not data the shop owns. If a shop ever sees this page (they shouldn't), the data is Kvick's, not theirs.

See also

  • Security model — the auth + role gates this rides on
  • Audit-everything — partnership_change_log is the duty-specific timeline, complementing the global audit chain