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.
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 it —
tom,james, both, orother(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:
| Table | Rows | Purpose |
|---|---|---|
partnership_duty_state | One per duty id | Current assignment / outsource / phase / notes |
partnership_custom_items | User-added duties | Title + category id |
partnership_custom_cats | User-added categories | Label + sort order |
partnership_change_log | Append-only | Every 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.
| Endpoint | Purpose |
|---|---|
GET /api/partnership | Full snapshot: state + custom items + custom cats + change log |
PATCH /api/partnership/duty/:duty_id | Partial update (any subset of tom/james/other/outsource_to/phase/notes) |
POST /api/partnership/custom-items | Add a custom duty |
POST /api/partnership/custom-categories | Add a custom category |
POST /api/partnership/log | Append a log entry (the page itself drives this; it's not auto-derived from the PATCH) |
DELETE /api/partnership/log | Clear the log (owner-only; deliberately destructive) |
POST /api/partnership/reset | Wipe 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/partnershipevery 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