Skip to content

Administration — User Journeys

Last verified: S210 A5-administration (29 April 2026) against S195-S209

  • kh-prod-readiness-S10/S11/S12/S13.

Administration covers the day-to-day jobs the admin (and, for a subset, the editor) performs to keep Canonical healthy: team management, taxonomy + tag-morphology curation, content ownership, governance configuration, layer vocabulary, notification preferences, pipeline health monitoring, and the operator-side jobs that run alongside the product (taxonomy sync, GDPR export, advisor review).

Most journeys live inside /settings. A few — Provenance pipeline health, Quality Review, MCP-driven governance review — live on dedicated routes. Operator jobs (GDPR export, advisor lint, row-count diff, staging refresh) are CLI-driven and ride on the runbooks in docs/runbooks/ and docs/handover/.

Entry PointRouteAccessible By
Settings/settingsAll roles (sections vary by role)
Provenance/provenanceAdmin only
Quality Review queue/reviewAdmin (Editor for content review)
MCP governance toolgovernance.review MCP toolAdmin/Editor authenticated MCP client
GDPR export CLIbun run scripts/export-user-data.tsOperator (Liam) with prod service-role key
Staging refresh / DB rebuildsupabase db push --linked + seed-e2e-users.tsOperator
Supabase advisor lintbun run scripts/run-supabase-advisors.tsOperator + CI nightly

/settings shows three groups: Personal (Profile, Organisation, Connections), Content Management (Content Organisation, Content Owners, Organisations & People, Guides, Tag Morphology), and System (Team, Quality Review, Reviewer Assignments, Provenance). Editors see Personal plus admin/editor-flagged sections (Organisation, Tag Morphology); viewers see Personal only.

Actor: Admin Goal: Keep the core taxonomy in sync with domain language shifts and land the change end-to-end (DB → classifier prompt → snapshot → plugin). Preconditions: Authenticated Admin.

  1. Access Categories tab
    • Route: /settings?section=content-organisation&tab=categories
    • Component: ContentOrganisationSectionTaxonomySection
    • User sees: Tree view of taxonomy_domains and taxonomy_subtopics with create/edit/delete controls. A TaxonomyDriftBanner surfaces when taxonomy_sync_state.last_sync_hash does not match the DB- computed hash.
  2. Edit a domain or subtopic
    • Action: Add/rename/disable a domain or subtopic.
    • Result: PATCH /api/taxonomy/domains/[id] (or subtopic equivalent) persists the change. The drift banner appears (or stays) until the downstream sync lands.
  3. Run taxonomy sync
    • Action: Press the “Sync taxonomy” action exposed by the drift banner.
    • Result: POST /api/admin/taxonomy-sync compares hashes; on mismatch it inserts a running row in pipeline_runs and dispatches the GitHub Actions taxonomy-sync.yml workflow with the pipeline run ID.
  4. Workflow regenerates artefacts
    • The workflow regenerates lib/ai/skills/classification.md, scripts/tests/fixtures/taxonomy_snapshot.json, and the plugin bundle, commits them, and calls POST /api/admin/taxonomy-sync/callback with the run outcome — flipping the pipeline_runs row to completed/failed and updating taxonomy_sync_state.last_sync_hash.
  5. Verify sync state
    • Route: /settings?section=content-organisation&tab=categories
    • User sees: Drift banner cleared. Optional confirmation via GET /api/admin/taxonomy-sync/status.
  • Hash already in sync: The dispatch endpoint returns { dispatched: false, reason: 'in_sync' } and records a no-op pipeline_runs row.
  • GitHub dispatch fails: pipeline_runs flips to failed, Sentry fires, and the route returns 502 with the actionable error.
  • Two admins editing concurrently: Last write wins; the drift banner remains until a sync run completes.

Journey 2: Team Management — Invite + Role Change

Section titled “Journey 2: Team Management — Invite + Role Change”

Actor: Admin Goal: Invite a new colleague and promote them from viewer to editor. Preconditions: Authenticated Admin. The inviting domain must satisfy the hook_restrict_signup_to_phew_domain allowlist (currently @phew.org.uk only — see docs/reference/auth-hooks.md).

  1. Invite by email
    • Route: /settings?section=team
    • Component: TeamSection
    • Action: Fill the invite form (email, optional display name, role).
    • Result: POST /api/admin/users/invite creates the auth user via auth.admin.inviteUserByEmail(). The on_auth_user_created trigger seeds a viewer-default user_roles row AND inserts the user_profiles mirror row in one body.
  2. Wait for first sign-in
    • The invitee follows the magic-link email. The auth-hook gate enforces the domain allowlist; non-Phew emails reject with HTTP 403.
  3. Promote to editor
    • Route: /settings?section=team
    • Action: On the user row, select the role dropdown and switch viewereditor.
    • Result: PATCH /api/admin/users/[userId] updates user_roles.role. The promoted user can now edit content, manage review queues, and see EditorView layouts.
  • Already-active user, role change only: Skip step 1.
  • Display name update: Same PATCH endpoint accepts display_name. The TeamSection renders display names via get_user_display_names() (mirrors auth.users via user_profiles — no GoTrue dependency).
  • Pipeline service account row (PIPELINE_SYSTEM_USER_ID) is filtered at the DB layer in GET /api/admin/users so the Team list shows humans only.
  • auth.admin.listUsers() 500s (S156-class GoTrue regression): the bulk read of user_profiles + user_roles still resolves; last_sign_in_at degrades to NULL. The UI displays “Never” rather than surfacing an error.
  • Role lookup DB failure: getAuthorisedClient returns role_lookup_failed → HTTP 500 (not silent downgrade to viewer).

Actor: Admin Goal: Deactivate a colleague who has left the organisation. Preconditions: Authenticated Admin.

  1. Open Team section
    • Route: /settings?section=team
    • Component: TeamSection
    • User sees: Single responsive flex-row list (TeamMemberRow); each row carries display name, email, role, last sign-in, and an inline ghost Deactivate button.
  2. Confirm deactivation
    • Action: Click Deactivate → confirmation AlertDialog fires.
    • Result: DELETE /api/admin/users/[userId] deactivates the user.
  3. Reassign content ownership (manual follow-up — not automated)
    • Route: /settings?section=content-owners
    • Component: ContentOwnerManagement
    • Reassign any items previously owned by the deactivated user.
  • Self-deactivation: Currently not gated client-side. The API route must enforce — see Current Limitations.
  • Notification sweep: Outstanding notifications rows for the deactivated user remain until the 24-hour expires_at default lapses.

Journey 4: Content Organisation — Tags + Layers

Section titled “Journey 4: Content Organisation — Tags + Layers”

Actor: Admin (Tag Morphology also Editor) Goal: Curate tags and depth-level layer vocabulary. Preconditions: Authenticated Admin (or Editor for Tag Morphology).

  1. Manage tags (clean-up)
    • Route: /settings?section=content-organisation&tab=tags
    • Component: TagsSectionTagsCleanup (auto-selected when duplicates exist) or TagsBrowse.
    • User sees: Duplicate clusters, domain-grouped views, bulk-action toolbar; or virtual-scrolled per-tag CRUD with merge/rename/delete.
    • Result: PATCH /api/tags/[id] or DELETE /api/tags/[id].
  2. Curate tag morphology drift queue (S195)
    • Route: /settings?section=tag-morphology
    • Component: TagMorphologySection
    • User sees: Pending drift flags from the corpus regression eval (stored_tag vs proposed_canonical, with usage_count and affected_content_ids).
    • Action: Triage each flag — accept (apply canonical), add_override (record domain-specific exception), or dismiss.
    • Result: PATCH /api/admin/tag-morphology/flags/[id] records the decision plus decided_by + decided_at + decision_rationale.
  3. Manage layer vocabulary
    • Route: /settings?section=content-organisation&tab=depth-levels
    • Component: LayersSection
    • Action: Add/rename/reorder/disable layer keys.
    • Result: POST/PATCH /api/layers[/id] updates layer_vocabulary. Affects content depth classification and the LayerSwitcherNav.

Actor: Admin Goal: Tune freshness thresholds, review cadences, and per-domain auto-flag policies. Preconditions: Authenticated Admin.

  1. Open Governance settings
    • Route: /settings?section=governance (sidebar label “Quality Review”)
    • Component: GovernanceSection
    • User sees: Per-domain configuration form for review cadence, freshness thresholds, auto-flag toggle, reviewer ID, timeout.
  2. Update rules
    • Action: Edit thresholds or toggle auto_flag_on_freshness_transition.
    • Result: POST /api/governance updates governance_config. Cron jobs (freshness-transitions, review-cadence, quality-score) read these values on next run.

Actor: Admin Goal: Assign or change content ownership for governance accountability. Preconditions: Authenticated Admin.

  1. Open Content Owners
    • Route: /settings?section=content-owners
    • Component: ContentOwnerManagement
    • User sees: List of content items with assigned owners.
  2. Assign or reassign owner
    • Action: Select a user from the dropdown.
    • Result: Ownership recorded on content_items.content_owner_id. The assigned user receives review_overdue notifications when the review-cadence cron fires for items past next_review_date.

Actor: Admin Goal: Check pipeline health — most recent runs, durations, failures. Preconditions: Authenticated Admin.

  1. Open Provenance
    • Route: /provenance (linked from the Settings sidebar Provenance entry)
    • Component: PipelineHealthTab in components/provenance/
    • User sees: Rollup summary cards, recent pipeline_runs with status, duration, and any error message; time-range and pipeline-kind filters.
  2. Drill into a failure
    • Component: PipelineFailureDrawer
    • User sees: Error detail, originating cron schedule (e.g. freshness-transitions, review-cadence, intelligence-poll), result JSON.
  3. Cross-reference per-item provenance
    • Tab: per-item
    • User pastes a content item UUID and sees AI cost figures (embedding, classification, summarisation), models used, and processing history.
    • Per AI-visibility policy, /provenance is the only surface that shows model names + token counts + cost.
  • Audit feed: Tab audit shows the activity feed lifted from the old Settings Activity section. /activity and /settings?section=activity redirect to /provenance?tab=audit.
  • Verification PDF export: ExportAuditPdfButton triggers GET /api/admin/provenance/export/verification-history — A4 monthly PDF rendered via React-PDF; download logged via recordPipelineRun().

Actor: All roles (Profile section is the entry) Goal: Toggle email notification categories. Preconditions: Authenticated user.

  1. Open Profile
    • Route: /settings?section=profile
    • Component: ProfileSectionNotificationPreferences
  2. Toggle preferences
    • Action: Flip Weekly Change Report, Review assignments, or Owned content flags.
    • Result: PUT /api/notifications/preferences upserts via sb(). All three default ON (silence-first with release valve).

The NotificationPreferences schema is strict — empty body / unknown fields / all-undefined patches reject. Backend cron and digest jobs query user_notification_prefs directly to decide whether to dispatch.


Actor: Admin / Editor (Editor for content review only) Goal: Triage pending governance review items. Preconditions: Authenticated Admin or Editor.

  1. Open Quality Review
    • Route: /review
    • Surfaces items with governance_review_status = 'pending' plus items flagged review_overdue by the review-cadence cron.
  2. Approve / request changes / revert
    • Action: Pick one of three actions per item.
    • Result: POST /api/governance/review (or the symmetric governance.review MCP tool) updates content_items.governance_review_status. On approve, cadence auto-renewal advances next_review_date to GREATEST(current, today) + review_cadence_days and stamps verified_at.

The MCP route uses the same helper so renewal is consistent across web + LLM-driven review surfaces.


Journey 10: GDPR Data Subject Export (Operator-Side)

Section titled “Journey 10: GDPR Data Subject Export (Operator-Side)”

Actor: Operator (Liam) — admin role on Vercel deploy + holder of the prod service-role key. Goal: Fulfil a verified UK GDPR Article 15 (right of access) or Article 20 (right to data portability) request within the statutory one-calendar-month deadline. Preconditions: Verified subject identity per docs/handover/gdpr-data-export.md §2.

  1. Verify identity (manual)
    • Confirm the requester demonstrates ≥2 of: email control, account knowledge, photo ID. Photo ID deleted immediately after verification.
  2. Run export script
    • Command: bun run scripts/export-user-data.ts --env=prod --user-id <uuid> --output ./exports/
    • Or --email <addr> for email-keyed lookup.
    • Article scope flag: --article=15 (default, full PII inventory) or --article=20 (portability subset).
  3. Inspect output bundle
    • Output dir: <output>/<uuid>-<timestamp>/
    • Contains: per-table JSON files (~20 PII tables), CSV summaries (UTF-8 BOM), Markdown index, SHA-256 manifest.
  4. Send bundle
    • Deliver to subject via secure channel (encrypted email, signed download).
  • Subject not found: Exit code 1 (no auth.users row).
  • Export error: Exit code 2; investigate per runbook §4.
  • Statutory deadline at risk: Notify subject of Article 12 §3 two- month extension before deadline expires.

The script is read-only against prod. Sandbox-stall fallback: re-invoke with dangerouslyDisableSandbox: true per the Bun-fetch HTTP 204 gotcha.


Journey 11: Supabase Advisor Review (Operator-Side)

Section titled “Journey 11: Supabase Advisor Review (Operator-Side)”

Actor: Operator Goal: Triage a new finding surfaced by the nightly advisor lint or PR-blocking lint workflow. Preconditions: Operator with SUPABASE_ACCESS_TOKEN PAT.

  1. Receive failure signal
    • CI: Supabase advisors workflow fails on a PR or scheduled run.
    • Or: Operator runs bun run scripts/run-supabase-advisors.ts --env=prod locally.
  2. Inspect the diff
    • Script output lists new findings vs the committed baseline at docs/audits/kh-production-readiness-phase-1/supabase-advisor-baseline.json.
  3. Decide: fix or rebaseline
    • Fix: Add the missing REVOKE EXECUTE, RLS policy, FK index, etc. The next migration replay + advisor lint should clear the finding.
    • Rebaseline: Run the script with --capture-baseline to overwrite the baseline JSON. Only do this when the finding is intentional and reviewed.

Journey 12: Staging Refresh / Database Rebuild (Operator-Side)

Section titled “Journey 12: Staging Refresh / Database Rebuild (Operator-Side)”

Actor: Operator Goal: Reset the persistent staging Supabase branch (turayklvaunphgbgscat) to known-good state after schema drift, monthly cadence, or post-incident. Preconditions: production-readiness branch HEAD ≥ main; all migration files present locally.

  1. Pre-flight
    • git status clean on the prod-readiness worktree.
    • Capture pre-reset migration counts via mcp__supabase__list_migrations for both prod and staging refs.
  2. Reset
    • Command: /opt/homebrew/bin/supabase link --project-ref turayklvaunphgbgscat && /opt/homebrew/bin/supabase db push --linked
    • On protected branches (staging is protected), mcp__supabase__reset_branch is a no-op — db push --linked is the canonical mechanism (per S4 empirical correction in the runbook).
  3. Re-seed test users
    • Command: bun run scripts/seed-e2e-users.ts
    • Idempotent provisioning via auth.admin.createUser().
    • verifyPipelineUserShape() returns exit code 2 if the S156 corrective migration is needed.
  4. Verify parity
    • Command: bun run scripts/verify-user-profiles-parity.ts --env=staging
    • Compares user_profiles count to count_auth_users() RPC.
    • Optional: bun run scripts/db-row-count-diff.ts --source=prod --target=staging for per-table row-count diff with allowlist.

Full procedure: docs/runbooks/staging-refresh.md.


Settings SectionViewerEditorAdmin
ProfileYesYesYes
OrganisationNoYesYes
ConnectionsYesYesYes
Content OrganisationNoNoYes
Content OwnersNoNoYes
Organisations & PeopleNoNoYes
GuidesNoNoYes
Tag MorphologyNoYesYes
TeamNoNoYes
Quality ReviewNoNoYes
Reviewer AssignmentsNoNoYes
Provenance (out-link)NoNoYes
Operator-side capabilityViewerEditorAdminNotes
GDPR data export scriptNoNoYes (operator)Requires prod service-role key; run on a trusted host
Supabase advisor lintNoNoYes (operator)Requires SUPABASE_ACCESS_TOKEN PAT
Staging refresh / db push --linkedNoNoYes (operator)Requires POSTGRES_PASSWORD + project-link
Cron route invocation (Bearer secret)NoNoYes (cron)Authorization: Bearer <CRON_SECRET> header from Vercel Cron

Editors gain access to Organisation and Tag Morphology sections; viewers still see only the Personal group (Profile + Connections). The “For developers” accordion inside ConnectionsSection is gated by useUserRole().canAdmin. Notification preferences (NotificationPreferences) live inside the Profile section and are available to all roles (S189 P1-18).

  • Settings forms (taxonomy, tags, layers, governance) perform direct updates without versioning/history tracking on admin metadata schemas.
  • Content owner reassignment after deactivation is manual — no automated redistribution.
  • Self-deactivation is not gated client-side; the API route should reject self-DELETE.
  • The Provenance cost and disputes tabs are placeholder stubs.
  • Auth-hook dashboard wiring (hook_restrict_signup_to_phew_domainbefore-user-created event) is NOT captured in SQL — must be re-configured after any project reset.
  • Multi-client domain allowlist is hardcoded; table-driven allowlist is tracked as backlog OPS-29.
  • E2E smoke / MCP eval / migration replay CI gates currently sit with continue-on-error: true until the staging eval-fixture seed lands (roadmap §9.16.10).
  • Branch protection enforcement gates on the GitHub Pro upgrade — checks run and report regardless (docs/runbooks/ci.md §3).
  • Technical reference: docs/product-functionality/administration/technical.md
  • Workflows: docs/product-functionality/administration/workflows.md
  • Auth hooks: docs/reference/auth-hooks.md
  • Local development: docs/runbooks/local-development.md
  • Staging refresh: docs/runbooks/staging-refresh.md
  • GitHub Environments: docs/runbooks/github-environments.md
  • CI runbook: docs/runbooks/ci.md
  • GDPR export: docs/handover/gdpr-data-export.md
  • Schema reference: docs/reference/SCHEMA-QUICK-REFERENCE.md
  • AI-visibility policy: docs/reference/ai-visibility-policy.md