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>
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:
workspacesobjectsmarkdown_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. Setsarchived_at. Cascades to a background job that flipsarchived_aton allobjectsandmarkdown_backlog_itemsfor that workspace. (For now, do it inline in the same transaction; revisit if it ever blocks.)workspaces.restore({ workspaceSlug })— owner only. Setsarchived_at = nulland 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:
iduuid pkworkspace_iduuid not null (indexed)actor_user_iduuid null (null = system actor, e.g. markdown importer)actionvarchar not null (e.g.object.create,object.update,member.invite,workspace.archive)target_typevarchar not null (e.g.object,workspace,workspace_member)target_iduuid null (nullable because some actions don't target a single row)metadatajsonb null (small structured payload — keep it small, don't dump full row state here)created_attimestamptz 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 = nullor 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_atto the 3 tables + partial indexes. - Add
audit_logschema. - Generate and commit migration.
- Add
archiveandrestoreprocedures to the workspaces router. - Implement
recordAudithelper and call from every mutation inobjects,workspaces,invites, and the markdown backlog import path. - Add
archived_at IS NULLto every existing list-procedure (audit existing routers). - Add
/[workspaceSlug]/settings/auditpage (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_logrow is written for every mutation inobjectsandworkspaces. - Audit view renders paginated rows scoped to the current workspace.
Links to related Epic / Plan
- Epic:
./Epic-tenant-lifecycle.md - Plan:
../Plan-multitenant-saas-hardening.md