Velvet UI Cluster Sprint Plan — 2026-05-20 UTC
T-3 to v1 launch (2026-05-23 UTC) Operator: Kristerpher PM review date: 2026-05-20 UTC Parent epic: #907
1. Scope + Context
Operator has authorised dispatch of the full Velvet UI cluster (6 cards: #911, #912, #949, #952, #953, #954). This document records the PM review findings, card-by-card dispositions, dependency ordering, realistic v1 cutline, and blocking questions.
What is already shipped
-
908 CLOSED: Velvet Heroku app pair + Postgres add-on (raxx-velvet-prod / raxx-velvet-staging) deployed.
-
909 CLOSED:
GET /tokens/{name}read-through proxy. -
910 CLOSED:
rotation_jobsPostgres schema (v1 shape). -
945 CLOSED: Subscription manifest loader + subscriber registry.
-
946 CLOSED: BusAdapter base + InfisicalWrite adapter.
-
950 CLOSED: Postmark sender-token adapter.
-
917 CLOSED: First console callsite migrated to Velvet.
-
964 CLOSED: Velvet admin UI with Google OIDC auth.
V2 schema migrations (001-003) exist at velvet/db/migrations/ — rotation_jobs_v2 + rotation_job_consumers tables and indexes. These supersede #910's v1 schema but have not been confirmed shipped to production; the feature-developer must verify and run these before #949 or #911 work is meaningful.
2. Architecture Drift Findings
Finding 1 — #911 body is stale (v1 scope, v2 additions required)
911's issue body describes the v1 kickoff endpoint: POST /tokens/{name}/rotate returning {job_id, status: "pending"}. The 2026-05-03 revision comment on #911 correctly identifies the v2 additions required per v2-rotation-flows.md:
- Request body must accept
flow_type(operational|testing|revocation) andforce_revokeflag. - Return shape changes:
status: "init"not"pending". - Three new endpoints needed:
POST /tokens/{name}/rotations/{job_id}/stage,GET /tokens/{name}/rotations/{job_id}(full consumers array),GET /tokens/{name}/rotations/{job_id}/stream(SSE).
The issue body has never been updated to reflect this. The acceptance criteria in #911 test the v1 shape (no flow_type, no stage endpoint, no SSE). The v2 requirements are in comments only, not in the card body. This creates dispatch risk: a feature-developer reading only the body will build the wrong thing.
Disposition: NEEDS-GROOMER-PASS — body must be updated to v2 scope before dispatch. The groomer should rewrite AC against the v2 API contract in v2-rotation-flows.md Section 4.
Finding 2 — #912 is superseded by #974
912 (service-token auth middleware + rotation-authz matrix) describes an M1 stub using a single VELVET_API_KEY env var. Issue #974 (feat(velvet/auth): Phase 5 — Velvet B10 auth middleware against RBAC v2 permission names) is explicitly scoped as the production-grade replacement for #912's stub, aligned to ADR-0043 and the RBAC v2 schema. #974 is currently OPEN, blocked.
Shipping #912 and then #974 as two sequential PRs is the correct sequencing. However #912's risk section acknowledges the stub is not production-grade, and it is marked sprint:next with no blocked label despite #974's dependency on the RBAC v2 phase being in progress.
Disposition: READY-FOR-DEV — #912 ships the M1 stub. Its PR must include a TODO comment citing #974 so the replacement is visible to reviewers. The stub is acceptable for v1 launch as long as #974 is on the post-launch plan. Recommend adding not-blocking-launch label only if Kristerpher confirms the stub is acceptable for v1.
Finding 3 — #949 depends on v2 schema migration being live
949 requires rotation_jobs_v2 + rotation_job_consumers tables (Section 3 of v2-rotation-flows.md). The v2 migration files exist in the codebase (velvet/db/migrations/001-003) but #910 (which closed) shipped the v1 schema. It is unclear whether the v2 migrations have been applied to Heroku databases. Before #949 dispatches, the feature-developer must confirm:
- V2 migrations run cleanly against
raxx-velvet-staging. - The
rotation_jobsv1 table (5-status enum) is replaced by the v2 table (21-status enum) without data loss on staging.
Since the Velvet app is pre-launch with no customer data, this is a DROP + CREATE replacement, not a live migration. But the task must be scoped explicitly in the PR.
Disposition: READY-FOR-DEV — card is well-specified and groomed. Size L is warranted. Dependency on v2 schema migration must be confirmed live on staging before PR merges to main.
Finding 4 — velvet_v2_rotation feature flag not in feature_flags.yaml
v2-rotation-flows.md Section 8 specifies that POST /rotate is gated behind FLAG_VELVET_V2_ROTATION. This flag does not currently exist in backend_v2/api/feature_flags.yaml. Per the memory rule feedback_new_flag_needs_b1_migration_same_pr, any PR adding a flag to the YAML must include a B1 promotion migration in the same PR or the B1 gate fails. This affects #911 (the kickoff endpoint) and any card that reads the flag.
Disposition: Blocker on #911 — the feature-developer picking up #911 must add velvet_v2_rotation (and velvet_v2_rotation_ui for #952) to feature_flags.yaml AND include the corresponding B1 promotion migration in the same PR.
Finding 5 — UI cards (#952, #953, #954) will add new console static assets
Cards #952 and #953 will require new CSS/JS for the multi-panel stage wizard modal. Per memory feedback_asset_manifest_layer_a, any PR touching console/app/static/** must run bash scripts/ci/update-asset-manifest.sh and commit docs/asset-manifest.json in the same PR or merge blocks. The feature-developer on #952 and #953 must be explicitly instructed to follow ADR-0051 Layer A. This is not currently called out in either card.
Disposition: Flag for card-groomer on #952 and #953 — add a "Dev notes" section to each card body noting the asset manifest requirement.
Finding 6 — #954 TOTP gate requires Velvet TOTP seed access
954 includes a TOTP gate on Stage 3 revoke. The TOTP seed must be accessible to Velvet's process. The risks section identifies this: "TOTP seed stored in Velvet's own vault path." The current Velvet codebase has no TOTP validation code. This is a non-trivial addition to what looks like a UI/auth card. If TOTP validation is a net-new library integration (pyotp or similar), the feature-developer must also handle vault secret path provisioning. This could silently inflate the card's scope.
Disposition: READY-FOR-DEV — but the feature-developer should be told explicitly: if TOTP validation is not already implemented in the Velvet codebase, scope it as a sub-task in the PR. The card's current AC is testable as written; no rewrite needed.
Finding 7 — NV-series internal references (#949 → NV8, #952 → NV9, etc.)
949 references "NV8 (modal UI)" and "NV9 (subscriber table)" as downstream dependencies. These internal NV codes map to: NV5=#949, NV8=#952, NV9=#953, NV10=#954. These are consistent but not cross-linked by issue number in the card bodies, which creates navigation friction for feature-developers. The groomer should add Blocks: #952, #953, #954 explicitly to #949's Related section.
3. Dependency Order
#912 (auth middleware stub) — no blockers; can dispatch now
|
+-- #911 (kickoff API + stage endpoints + SSE)
Depends on: #912 (auth), v2 schema migration confirmed live on staging
flag velvet_v2_rotation must be added to feature_flags.yaml in same PR
|
+-- #949 (three-stage flow state machine)
Depends on: #911 (kickoff endpoint + stage action endpoint)
Depends on: v2 schema migration (rotation_job_consumers) live
Largest card (size L); ~3-4 days for a focused developer
|
+-- #953 (subscriber status table component)
Depends on: #949 (SSE stream + rotation_job_consumers data shape)
Size S; can be built against mocked SSE for parallel delivery
|
+-- #952 (three-stage modal shell)
Depends on: #949 (flow runner live), #953 (status table component ready)
flag velvet_v2_rotation_ui must be in feature_flags.yaml
|
+-- #954 (yaml-driven revocation auth gate)
Depends on: #949 (proceed_revoke action), #952 (modal context — renders within)
Can be built in parallel with #952 if TOTP lib is pre-confirmed available
Confirmed dependency order:
-
912 (can dispatch immediately)
-
911 (after groomer rewrites body; dispatch alongside #912 or immediately after)
-
949 (after #911 + #912 merge; largest card)
-
953 (after #949 merges; can start against mocked data if #949 takes long)
-
952 (after #949 + #953 ready)
-
954 (after #949; parallel with #952)
4. T-3 Realism Assessment
Available calendar: 2026-05-20 to 2026-05-23 UTC = 3 days.
Tier 1 — v1 blockers (must ship by 2026-05-23 UTC)
These cards are the minimum for Velvet rotation to be operator-usable at launch:
| Card | What it delivers | Estimated effort | Assessment |
|---|---|---|---|
| #912 | Auth middleware stub | 0.5 days | Shippable by 2026-05-21 |
| #911 | Kickoff + stage + SSE endpoints | 1.5 days (after groomer pass) | Shippable by 2026-05-22 if groomer runs same day |
| #949 | Three-stage flow state machine | 3+ days (size L) | DOES NOT FIT T-3 |
The honest cutline: #949 cannot ship by 2026-05-23 UTC. It is size L, depends on #911+#912 merging first, and involves a complex orchestration engine (three distinct state machines, SSE stream, parallel fan-out, per-consumer audit rows). A minimum realistic timeline for #949 is 3 working days from dispatch, which puts it at 2026-05-26 at the earliest.
Tier 2 — v1-adjacent (valuable but not blocking launch)
| Card | What it delivers | Assessment |
|---|---|---|
| #953 | Subscriber status table component | Size S; shippable post-#949 |
| #952 | Three-stage modal shell | Size L; depends on #949 + #953; post-launch |
| #954 | Yaml-driven revocation auth gate | Size M; depends on #949; post-launch |
Recommended v1 subset
Ship by 2026-05-23 UTC:
- #912 — auth middleware stub (M1 production-grade stub; post-launch #974 hardens it)
- #911 — kickoff + stage + SSE API surface (after groomer body rewrite; gated behind velvet_v2_rotation flag = OFF at launch)
Ship post-launch (wave 1 post 2026-05-23): - #949 — flow state machine (target 2026-05-27) - #953 — subscriber table (target 2026-05-28) - #952 — three-stage modal (target 2026-05-30) - #954 — revocation auth gate (target 2026-05-30)
This ships the API contract and auth foundation at launch with the flag off. The console UI and flow engine follow in the week after launch as a defined wave. Rotation is an operator-only infrastructure feature — it does not affect customer-facing product behaviour and does not block the 2026-05-23 launch.
5. Blocking Questions for Kristerpher
Q1 — v2 schema migration status (blocks #911 + #949)
The v2 migration files exist in the codebase but it is unclear whether 001_create_rotation_jobs_v2.sql has been applied to raxx-velvet-staging. Can you confirm via heroku run python -m velvet.db.migrate --list -a raxx-velvet-staging which migrations have run? If the v1 schema (#910) is still live, the feature-developer for #911 must include the v2 migration as part of that PR.
Q2 — Acceptable auth model at launch: single VELVET_API_KEY stub or wait for #974?
912 ships a single shared VELVET_API_KEY. #974 (RBAC v2 per-caller scoped tokens, currently blocked) is the production-grade replacement. The stub is documented and the risk is low for a pre-revenue service where Velvet is operator-only. Is the stub acceptable for v1 launch, or should the velvet_v2_rotation flag be held off until #974 ships? This determines whether #912 alone is sufficient or whether #974 must be on the v1 list.
Q3 — Full cluster desired post-launch even if not at v1? The operator authorised dispatch of the "full Velvet UI cluster." Given that #949 does not fit T-3, do you want all 6 cards dispatched immediately (with #949-#954 landing in the post-launch window), or should dispatch of the UI cards (#952, #953, #954) wait until #949 is confirmed merged to avoid wasted work in feature-developer worktrees?
6. Pre-Dispatch Actions Required
Before any feature-developer can be dispatched on these cards, the following must happen:
-
#911 groomer pass — body must be rewritten to v2 scope (flag the
flow_typeparam, stage endpoint, SSE stream, corrected response shapes, updated AC). The current body will produce wrong implementation if dispatched as-is. -
#952 and #953 groomer note — add "Dev notes: this PR touches
console/app/static/**; runbash scripts/ci/update-asset-manifest.shand commitdocs/asset-manifest.jsonin the same PR per ADR-0051 Layer A" to each card body. -
Operator answer on Q1 — confirm v2 schema migration status before #911 or #949 dispatch.
-
Operator answer on Q2 — confirms whether #912 stub is acceptable for v1 or whether #974 must be included.
7. Card-by-Card Disposition Summary
| # | Title | Disposition | v1 cutline | Notes |
|---|---|---|---|---|
| #911 | POST /rotate kickoff + stage + SSE | NEEDS-GROOMER-PASS | v1 (after groomer pass) | Body is v1-scope; v2 additions in comments only; flag must be added to feature_flags.yaml |
| #912 | Service-token auth middleware | READY-FOR-DEV | v1 | Stub acceptable; cite #974 in PR TODO |
| #949 | Three-stage flow state machine | READY-FOR-DEV | POST-LAUNCH | Size L; does not fit T-3; target 2026-05-27 |
| #952 | Three-stage rotation modal | READY-FOR-DEV | POST-LAUNCH | Depends on #949 + #953; asset manifest required in PR |
| #953 | Per-subscriber distribute-status table | READY-FOR-DEV | POST-LAUNCH | Depends on #949; asset manifest required in PR |
| #954 | Yaml-driven revocation auth gate | READY-FOR-DEV | POST-LAUNCH | Depends on #949; confirm TOTP lib available before dispatch |
Sprint plan authored 2026-05-20 UTC. card-groomer has been tagged on #911, #952, #953.