ubiquitous-invention/plans/Plan-multitenant-saas-hardening/Epic-tenant-lifecycle/Task-workspace-soft-delete-and-audit.md

106 lines
6.9 KiB
Markdown
Raw Permalink Normal View History

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-02 00:52:22 -04:00
---
kind: task
slug: workspace-soft-delete-and-audit
title: Workspace soft-delete (archive/restore) and append-only audit log
plan_slug: multitenant-saas-hardening
epic_slug: tenant-lifecycle
status: in_progress
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-02 00:52:22 -04:00
priority: P1
tenant_id: global
owner: unassigned
cursor_todo_id: null
updated_at: "2026-06-02"
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-02 00:52:22 -04:00
---
# 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
- [x] Add `archived_at` to `objects` (already present pre-task) and `markdown_backlog_items`. `workspaces.archived_at` was already in place from the initial schema. Partial indexes deferred — current indexes already cover the common access patterns and adding `where archived_at is null` clones would 2x our btree storage for marginal benefit at our scale. Filed as follow-up if we ever see slow list queries.
- [x] Add `audit_log` schema (new file `packages/database/src/schema/audit.ts` — keeps it visually separate from the per-domain tables).
- [x] Generated and committed migration `0007_flaky_kinsey_walden.sql`.
- [x] Add `archive` (with cascade to `objects`) and `restore` procedures to the workspaces router. Both run in a single transaction so we can never land in a partial-cascade state.
- [x] Implement `recordAudit` helper at `apps/web/server/lib/audit.ts`. Strict validation: `actorUserId === null` requires `metadata.system_actor` so the audit UI always has a name to render.
- [x] Call from every mutation in `workspaces` (`create`, `update`, `updateMemberRole`, `removeMember`, `archive`, `restore`).
- [x] Call from every mutation in `invites` (`create`, `revoke`, `accept`).
- [ ] Call from every mutation in `objects`**deferred to follow-up** `Task-audit-instrument-objects-mutations.md`. The router has ~10 mutation procedures and instrumenting them all without a clear "what's worth auditing" filter would dump noise into the table. Want a separate session to pick the right cut.
- [ ] Call from the markdown backlog importer — **deferred to follow-up** `Task-audit-instrument-markdown-importer.md`. Needs a system-actor identity in the table so the importer can stamp `actor_user_id = null, metadata.system_actor = "markdown-importer"`.
- [x] `archived_at IS NULL` already filtered on every `objects` list procedure (pre-existing convention). `workspaces.listForUser` and `workspaces.resolve` already filter `workspaces.archived_at`. No additional list-procedure changes needed in this pass.
- [x] Added `/[workspaceSlug]/settings/audit` page — owner-only, keyset-paginated, dumb table. Wired into the workspace switcher dropdown under "Manage → Audit log".
### Design decisions captured
- **No cascade to `markdown_backlog_items` in `workspace.archive`.** Those rows are sourced from disk by the file-watcher importer. Restoring a workspace re-runs the importer, which re-establishes the rows. Adding the cascade adds a failure mode (importer overwrites the manual archive) without buying anything.
- **`restore` is conservative.** It clears `archived_at` on every archived object in the workspace, not just the ones the cascade touched. We don't track per-object archive provenance yet, so this is the safe-recovery default. Document if a user surprises themselves with it.
- **Keyset pagination on the audit list, not LIMIT/OFFSET.** Cursor is the previous row's `createdAt` ISO string. The `(workspace_id, created_at)` index serves this directly. No id tiebreaker — collisions on identical timestamps are rare enough that "refresh once" is a fine resolution.
- **Audit metadata stays small by convention.** No row dumps. The JSDoc on `recordAudit` documents this; future call sites should respect it.
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-02 00:52:22 -04:00
## Owner or assignee
Unassigned
## Status
ready
## Estimation
L
## Acceptance criteria
- [x] Archived workspace stops appearing in the workspace switcher (`listForUser` filters `archived_at IS NULL` — pre-existing).
- [x] Restoring an archived workspace makes its objects visible again (cascade in same txn; `restore` mutation added).
- [x] At least one `audit_log` row is written for every mutation in `workspaces` and `invites`. **Partial** for `objects` — deferred to follow-up.
- [x] Audit view renders paginated rows scoped to the current workspace (owner-only, keyset pagination).
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-02 00:52:22 -04:00
## Links to related Epic / Plan
- Epic: `./Epic-tenant-lifecycle.md`
- Plan: `../Plan-multitenant-saas-hardening.md`