ubiquitous-invention/plans/Plan-multitenant-saas-hardening/Epic-tenant-lifecycle/Task-workspace-soft-delete-and-audit.md
Randall Stillwell 778fe1d321 plans: scaffold daily-driver-finish, saas-hardening, agent-coordination
Three new plan trees that fill in the gaps surfaced during repo review.
Together they map out what remains between the current scaffold-with-stubs
state and a daily-usable, multitenant, agent-coordinated app.

* Plan-daily-driver-finish (P0): turn stubs into real data. Five tasks
  covering the lint/shared-types breakage, hardcoded dashboard mocks,
  AI-page setTimeout placeholder, post-signin landing decision, and a
  cross-browser collab smoke test against the deployed Hocuspocus
  instance.

* Plan-multitenant-saas-hardening (P1): everything multitenant needs
  beyond what Plan-multitenant-cursor-sync already covers. Invites and
  role management, soft-delete + append-only audit log, rate limits on
  the auth + mutation hot paths, and a Vitest + GitHub Actions test
  foundation so PRs can't ship red.

* Plan-agent-coordination (P2): the layer that makes a Task-*.md
  runnable, not just readable. Adds workflow_prompt with task -> epic
  -> plan inheritance, an agent_runs table for auditable sessions, and
  two new MCP tools (claim_task / complete_task) that replace the
  freeform update_object composition agents do today. Includes an
  intentionally-deferred Epic-optional-orchestrator that captures the
  Symphony-shaped runner as a decision point rather than an immediate
  build.

Each task is bead-scale (one focused Cursor session) with explicit
in-scope, out-of-scope, and anti-goal sections so a future agent can
pick up a single Task-*.md and start without scrollback context.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 23:52:22 -05:00

4 KiB

kind slug title plan_slug epic_slug status priority tenant_id owner cursor_todo_id updated_at
task workspace-soft-delete-and-audit Workspace soft-delete (archive/restore) and append-only audit log multitenant-saas-hardening tenant-lifecycle ready P1 global unassigned null 2026-06-01

Task summary

Two related changes shipped together because they share the "we need a paper trail" motivation: soft-delete for workspaces (and the major tenant-scoped tables) and an append-only audit_log that records who did what.

Description

Soft-delete

Add archived_at timestamptz null to:

  • workspaces
  • objects
  • markdown_backlog_items

For each, add a partial index where archived_at is null on the same columns currently indexed (so the "active rows" filter stays fast).

Read-side convention: every workspace-scoped tRPC procedure adds archived_at IS NULL to its where clause by default. Add an includeArchived: boolean optional input to list-procedures that opt-in to showing archived rows (settings → archive view).

Write-side:

  • workspaces.archive({ workspaceSlug }) — owner only. Sets archived_at. Cascades to a background job that flips archived_at on all objects and markdown_backlog_items for that workspace. (For now, do it inline in the same transaction; revisit if it ever blocks.)
  • workspaces.restore({ workspaceSlug }) — owner only. Sets archived_at = null and cascades the unset.

Don't hard-delete anything via the UI yet. Hard-delete is a separate task and a separate set of risks.

Audit log

New table audit_log:

  • id uuid pk
  • workspace_id uuid not null (indexed)
  • actor_user_id uuid null (null = system actor, e.g. markdown importer)
  • action varchar not null (e.g. object.create, object.update, member.invite, workspace.archive)
  • target_type varchar not null (e.g. object, workspace, workspace_member)
  • target_id uuid null (nullable because some actions don't target a single row)
  • metadata jsonb null (small structured payload — keep it small, don't dump full row state here)
  • created_at timestamptz default now (indexed (workspace_id, created_at desc))

Write path: a tiny helper recordAudit(db, { workspaceId, actorUserId, action, targetType, targetId, metadata }). Call from every mutation procedure. Don't auto-instrument via Drizzle middleware — be explicit so it's grep-able which mutations are audited and which aren't.

Read view: minimal — apps/web/app/(app)/[workspaceSlug]/settings/audit/page.tsx (new route) with a paginated table. Owner-only. Keep the UI dumb; this is a debugging surface, not a product feature.

Anti-goals

  • Don't try to write to the audit log from inside a non-procedure context (cron, importer) without an explicit actor_user_id = null or a synthetic "system" user. The point of the column is "who did this, for accountability" — fudging it defeats the purpose.
  • Don't add row-level retention policies in this task. The table can grow; we'll partition or roll up later.

Subtasks

  • Add archived_at to the 3 tables + partial indexes.
  • Add audit_log schema.
  • Generate and commit migration.
  • Add archive and restore procedures to the workspaces router.
  • Implement recordAudit helper and call from every mutation in objects, workspaces, invites, and the markdown backlog import path.
  • Add archived_at IS NULL to every existing list-procedure (audit existing routers).
  • Add /[workspaceSlug]/settings/audit page (owner-only, paginated).

Owner or assignee

Unassigned

Status

ready

Estimation

L

Acceptance criteria

  • Archived workspace stops appearing in the workspace switcher.
  • Restoring an archived workspace makes its objects visible again.
  • At least one audit_log row is written for every mutation in objects and workspaces.
  • Audit view renders paginated rows scoped to the current workspace.
  • Epic: ./Epic-tenant-lifecycle.md
  • Plan: ../Plan-multitenant-saas-hardening.md