T-HUB/docs/design/spec-review-workflow.md
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

13 KiB
Raw Blame History

Spec — Group Review & Signoff for Posts ("Editorial Workflow")

Status: DRAFT for proposals Owner: product owner Date: 2026-10-07 Inputs: user requirements, codebase inventory (explore agent), background research (Hootsuite/Sprout approval features, four-eyes/maker-checker compliance patterns)

1. What the user asked for (verbatim intent)

  1. Posts can be primed to go out, then reviewed and edited by others in the group.
  2. Roles: editors (can edit, can NOT post/approve) and reviewers (give the OK).
  3. System configurable so 2–3 people sign off on a post before it publishes.
  4. Reviewers can approve, or deny with reasoning (reason mandatory).
  5. A parallel chat per post with voting/approval buttons.
  6. Easy scrolling through existing working posts (the in-review queue).
  7. When posts publish, they are archived.
  8. Ability to make our own posts invisible or delete them — admin-only features.
  9. Think through the whole system; spec the needs.

2. Current system (facts from codebase inventory)

2.1 Publishing state machine

  • States in DB CHECK (migrations/006_publishing.sql:7-8): editing, in_review, approved, rejected, scheduled, published. TS mirrors: publishing.service.ts:8, web api-types.ts:64.
  • Transitions are guarded CAS UPDATEs (WHERE id=$1 AND state=$4) via transition() helper (publishing.service.ts:275-292):
    • editing→in_review submitForReview (perm approval.request)
    • in_review→approved approve (perm approval.grant, stamps reviewer_user_id)
    • in_review→rejected reject (perm approval.grant; reason goes ONLY into audit diff, not the draft row)
    • approved→scheduled schedule (inserts scheduled_publications, perm publication.schedule)
    • scheduled→approved cancelSchedule
    • scheduled→published by scheduler activity (publish.activity.ts:181-184)
  • Approval record: NONE. Only drafts.reviewer_user_id (single reviewer, last-writer-wins) + audit events. No quorum structure exists.
  • rejected is terminal — there is NO resubmit path today. This feature must add one.
  • Temporal: one workflow scheduledPublish(idempotencyKey) proxying one activity, no signals/queries. API starts it fire-and-forget with startDelay; durable scheduled_publications row (pending/in_flight/done/failed) is source of truth; sweeper publishing_due_schedules() re-starts missed workflows.

2.2 Data model

  • drafts(id, workspace_id, author_user_id, post jsonb, state, reviewer_user_id, created_at, updated_at) — post payload = {text, mediaAssetIds[], visibility, scheduledAt?, providerMetadata}. No soft-delete, no hidden flag, no delete endpoint at all.
  • scheduled_publications — UNIQUE(draft_id) (one schedule per draft). publications — outcome rows (remote_id, idempotency_key).
  • All tables: FORCE RLS tenant_isolation on app.workspace_id + workspace-immutability triggers + sp_app grants. Drafts are workspace-scoped only — no group column.

2.3 RBAC

  • 10 roles seeded (002_rbac_seed.sql): Owner, Admin, Publisher, Editor, Approver, Analyst, Moderator, VideoPublisher, StreamOperator, BroadcastOperator.
  • approval.grant: only Owner/Admin/Approver. approval.request: Owner/Admin/Publisher.
  • Enforcement: RbacGuard + @RequirePermission() decorator; guard verifies HMAC session → workspace authz_epoch (fail-closed 401 on mismatch) → permission via role_permissions JOIN memberships(status='active') inside withTenant.
  • Roles are per-workspace only (memberships.role, UNIQUE(workspace_id,user_id) — one role per user per workspace). group_members has NO role column — no per-group roles exist.

2.4 Groups

  • Tables (008_groups.sql): groups, group_members (no role), group_connections, group_assets. Denormalized workspace_id + composite FKs prevent cross-tenant groups.
  • Epoch: any group_members change fires bump_authz_epoch(); sessions carry authzEpoch; guard compares live. Membership mutations mint a replacement token in the same tx ({token} response pattern).
  • Connection scoping: getVisibleConnections — Owner/Admin see all; others see union of group connections via their memberships. Guard-side userCanUseConnection reads role from DB.

2.5 Realtime & chat

  • No WS transport exists. realtime/gateway.ts is an authz port only (authorizeSubscription checks session, workspace, epoch, stream.view). No topics, no client code.
  • Web Inbox is a fixtures-only two-pane chat UI (conversation list + thread bubbles + reply box). Reusable as visual pattern, no backend.

2.6 Frontend

  • Compose (compose/page.tsx): tiptap editor, target chips, visibility, approval select, schedule; publish() chains create→submit→[approve]→[schedule] gated on can(); "Recent drafts" sidebar with STATE_TONE badges is the only post list today.
  • /approvals is a 10-line stub; /calendar stub.
  • Streams columns (components/streams/column.tsx): @tanstack/react-virtual useVirtualizer, j/k keyboard nav — reusable pattern for the working-posts queue.
  • Audit page (236 lines) with diff viewer exists.

2.7 Audit

  • audit_log hash-chained per workspace (audit_hash = sha256(prev|actor|action|target_id|epoch)), SECURITY DEFINER append fns, audit_verify_chain. Services write via appendTenantAudit (after-commit) or same-tx SELECT audit_append(...). Scheduler writes with actor_role='sp_scheduler'.

3. Background research — patterns to draw from

Social tools (Hootsuite, Sprout)

  • Workflow shapes: linear, tiered (multi-level), parallel, conditional. Sprout steps support Any vs All sign-off ("any 1 of step's approvers" / "all must approve").
  • Post-locking after approval: any post-approval edit re-triggers the chain. Hootsuite: "Once approved, the post is locked; changes send it back through approval."
  • Expired queue: if not approved by scheduledAt, publish FAILS, post moves to Expired (author re-schedules + resubmits). 90-day auto-reject of stale pending items.
  • Rejected → author edits → resubmits. Rejection notes surface to author.
  • Approval Activity panel: every edit (who/when) + threaded internal comments + current step; approvers can edit-before-approve; tag members; notifications (digest + hourly reminders).
  • Approvals inbox filters: Assigned-to-me, Created-by-me, Rejected, Expired.
  • Teams of approvers (not individuals) to avoid bottlenecks; duplicate-reviewer skip.

Four-eyes / maker-checker (compliance-grade)

  • Self-approval must be structurally impossible — server-side check approver ≠ author, dedicated error, not policy.
  • Approve a frozen snapshot, not a moving target: submit seals content (hash); edits after submit = new submission + new approval cycle; prior snapshots stay in audit trail. Reviewer sees the real diff.
  • Deny requires non-empty reason (empty reason refused); approvals/denials recorded with actor+timestamp+snapshot hash.
  • Eligibility re-checked at every transition (not just submit).
  • Parallel double-approval (Signavio): parallel split/join, either order, automatic gateway; enforce distinct reviewers at assignment.
  • Edge cases to model explicitly: reviewer leaves mid-review (votes revoked? reassigned?), vacation delegation, emergency admin override (logged as override), conflict discovered late.
  • State machine enforced in BOTH app logic and DB triggers (defense in depth).
  • Metrics: approval cycle time, approvals per reviewer (rubber-stamp detection).

4. Requirements (normative for proposals)

R1 Lifecycle

  • New/extended states covering: draft/editing → primed/submitted for review → in-review → approved (quorum met) → scheduled → published → archived; deny → needs-changes (back to author/editor with reasons); resubmit path (currently missing — rejected is terminal today).
  • If a scheduled time passes without quorum → expired state (post does NOT publish; author notified; re-prime after re-approval).

R2 Roles & permissions

  • Editor: can edit group posts (content), cannot approve, cannot publish.
  • Reviewer: can approve/deny with mandatory reason, cannot edit content.
  • Roles must be assignable per group (group-scoped), coexisting with workspace roles. Author can never approve their own post (structural, server-side). Admin/Owner retain override power (logged as override).
  • Existing approval.grant/approval.request permissions are the workspace-level seeds; extend, don't break, current 10-role model.

R3 Quorum signoff

  • Configurable N approvals required (2–3 typical) — decide where config lives (workspace default + per-group override? per-connection?) and who sets it (admin-only).
  • Parallel (any-N-of-eligible) semantics by default; justify if you add tiered/sequential mode.
  • Vote rules: one live vote per reviewer per submission; vote change allowed while in-review (with new reason) or immutable — pick and justify. Deny = single vote sends to needs-changes. Quorum met → auto-transition approved (→ scheduling continues if schedule set).

R4 Staleness & integrity

  • Content edits during review must NOT silently invalidate approvals: pick a model (frozen snapshot + new cycle on edit, vs edit-with-approval-dismissal + diff shown) and justify. Snapshot hash in audit log.
  • Reviewer must always see exactly what they're approving (rendered post + diff since last review).

R5 Review chat

  • Per-post thread: messages from group members + system events (submitted, edits, approvals/denials with reasons, state changes) rendered inline.
  • Approve / Deny+reason buttons in the chat surface for eligible reviewers.
  • Realtime delivery: no WS transport exists today — propose transport (WS gateway wiring, SSE, or polling) with epoch/authz integration; justify choice.

R6 Working-posts queue (UI)

  • Scrollable list of in-flight posts: state chips, quorum progress (e.g. "2/3 ✓"), filters (needs-my-review, needs-changes, expiring soon), keyboard-friendly (j/k pattern from Streams).
  • Post detail: rendered post preview + version diff + chat/vote panel + schedule info.

R7 Archive & moderation

  • Published posts land in an archive view (read-only, chat history preserved for record).
  • Hide (invisible to members; badge for admins) and delete (hard or soft — justify) of posts — space-admin-only, new permissions, hash-chained audit events, epoch-safe.
  • Decide moderation scope: archive-only or also drafts/working posts.

R8 Security & audit (mandatory repo patterns)

  • Everything through existing RbacGuard/@RequirePermission, FORCE RLS + tenant isolation on new tables, sequential migration 009_*, hash-chained audit events for every state transition and vote, epoch re-token where membership/role changes affect pending reviews (reviewer removed from group → their pending votes must be revoked/re-counted).

R9 Non-goals (this phase)

  • Email/external notifications (design the hook, don't build), mobile-specific UI, per-platform conditional routing, AI-assisted review.

5. Open design questions (proposals must answer explicitly)

  1. Group-scoped role storage: column on group_members vs separate group_member_roles table vs reusing workspace roles — tradeoffs?
  2. Snapshot model: full content copies per version vs content-hash + immutable version rows?
  3. Where does quorum config live and what's the precedence (workspace default → group override)?
  4. Chat transport: WS (wire the existing authz port) vs SSE vs polling — cost/complexity/ordering guarantees?
  5. Deny semantics: 1-deny-returns-to-author vs majority-deny vs configurable; what happens to other cast approvals on deny?
  6. Re-review after edit: full reset vs dismiss-only-changed-scope (recommend one, note the other as future)?
  7. Scheduled-time race: quorum completes at T-1min to scheduledAt — auto-publish or require manual re-confirm?
  8. Author is also the only reviewer in a tiny group — deadlock handling (admin override path)?
  9. Migration path for existing single-step approvals (reviewer_user_id backfill)?
  10. Should reviewers see who else approved before them (peer pressure) or vote blind until quorum?

6. Proposal deliverable format (each lane produces runs/wp5/proposal-<lane>.md)

  1. Executive summary — the 5 sharpest decisions you make.
  2. Data model — migration SQL sketch (009+, following repo conventions: FORCE RLS, tenant isolation, immutability triggers, sp_app grants, audit fns reuse).
  3. State machine — full state/transition table with guards and who can fire each.
  4. RBAC — new permissions, group-role mechanism, self-approval exclusion, admin override.
  5. Quorum engine — vote lifecycle, staleness/snapshot semantics, deny handling, race conditions.
  6. Review chat — data model, event types, transport decision, authz integration.
  7. UI plan — screens (queue, detail, archive), reuse of existing components, keyboard model.
  8. Edge cases — the ugly ones (reviewer leaves, deadline race, deadlock, epoch churn mid-vote).
  9. Audit & security — event list, chain integration, RLS notes.
  10. Phased implementation plan — WP-sized chunks with test gates.
  11. Test plan sketch — the 10 tests that would catch the worst regressions.

Constraints: NestJS + Postgres 16 + Temporal patterns as in repo; TS strict, no as any; follow existing naming/style; do not break the 10-role seed contract; proposals are designs — do NOT implement.