ubiquitous-invention/plans/Plan-multitenant-saas-hardening/Epic-tenant-lifecycle/Epic-tenant-lifecycle.md
Randall Stillwell 336a5890a8 feat(audit): append-only audit_log, workspace archive cascade + restore, audit view
Soft-delete cascade was the missing half of archive: stamping
workspaces.archived_at alone left objects visible to anyone with a
direct id. The cascade runs in one transaction so the partial state
isn't reachable, and restore inverts it for any archived row in the
workspace — provenance-blind on purpose until we have a use case
that needs to distinguish per-workspace from per-object archives.

audit_log keeps the keyset index on (workspace_id, created_at) and
the actor_user_id FK with onDelete set null. recordAudit() refuses
to write a null actor without a metadata.system_actor label so the
audit view always has something to render. workspaces and invites
mutations call recordAudit on success; objects-router instrumentation
and the markdown importer's system-actor flow are filed as P2
follow-ups because each needs a thoughtful "what's audit-worthy?"
pass, not mechanical wiring.

Settings → Audit log lives at /<slug>/settings/audit, owner-gated,
keyset-paginated. ACTION_LABELS is small on purpose; new actions
fall back to their raw key so missing a label degrades gracefully.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 15:35:16 -05:00

4.3 KiB

kind slug title plan_slug status priority tenant_id cursor_epic_id updated_at
epic tenant-lifecycle Tenant lifecycle — invites, roles, soft-delete, audit, rate-limits multitenant-saas-hardening ready P1 global null 2026-06-02

Epic objective

Cover the operational edges of running a multitenant app: who's in the workspace, what they did, can we recover from a mistake, and can we cap abuse.

In scope / out of scope

In scope

  • Multi-email identity on user profile (foundation — one user can own multiple verified emails).
  • Invite + role management (workspace_members already exists; add invite flow on top).
  • Smart invite-recipient autocomplete (members / pending / known users / new email).
  • Append-only audit_log table with write hooks.
  • Soft-delete on workspaces and core tenant-scoped tables.
  • Rate-limiting middleware on auth + mutation routes.

Out of scope

  • SSO group sync (later).
  • Per-tenant billing (separate plan).
Task Link
Multi-email identity on user profile ./Task-multi-email-identity.md
Workspace invites and roles ./Task-workspace-invites-and-roles.md
Invite recipient autocomplete ./Task-invite-recipient-autocomplete.md
Workspace soft-delete and audit ./Task-workspace-soft-delete-and-audit.md
Rate limit and abuse guardrails ./Task-rate-limit-and-abuse-guardrails.md

Follow-ups (filed, not in this convoy)

Task Link Why deferred
Manual email verification ./Task-manual-email-verification.md Add an email outside an OAuth provider; needs verification token + send + entry UI.
Disconnect linked email ./Task-disconnect-linked-email.md Has destructive edge cases (last verified email, primary swap).
Account merge ./Task-account-merge.md Merge two existing users who turn out to share an email — security-sensitive, not v1.
Audit-instrument objects mutations ./Task-audit-instrument-objects-mutations.md Needs a "which mutations are audit-worthy?" pass — not a mechanical instrumentation.
Audit-instrument markdown importer ./Task-audit-instrument-markdown-importer.md Importer needs a system-actor convention; pick fan-out policy (per-sweep vs per-row).
Distribute rate-limit to Redis ./Task-distribute-rate-limit-redis-backed.md Required before we scale apps/web beyond one pod.
Rate-limit workspace create/archive ./Task-rate-limit-workspace-create-and-archive.md Lower-impact than sign-in/invite; deferred to keep parent scope narrow.
Audit-instrument rate-limit trips ./Task-audit-instrument-rate-limit-trips.md audit_log writes on every trip — needs a "system workspace" call for unauthenticated trips.

Dependencies

  • Depends on: Plan-daily-driver-finish (don't harden surfaces that are still stubs).
  • Blocks: charging real money, onboarding non-trusted tenants.

Acceptance criteria

  • An owner can invite an email; the recipient lands in the workspace after sign-in.
  • Every tenant-scoped mutation in workspaces and invites produces an audit_log row. Objects-router mutations and the markdown importer are filed as follow-ups (need thoughtful "what's worth auditing?" passes, not mechanical instrumentation).
  • An archived workspace stops serving its data through tRPC and is restorable. (No 30-day retention policy yet — restoration is currently indefinite, which is more generous than the spec. A hard-delete-after-30-days follow-up can land separately if/when we need it.)
  • Credentials sign-in is rate-limited at the authorize callback. (See Task-rate-limit-and-abuse-guardrails for the deviation rationale — Auth.js v5 doesn't expose a route-handler hook before authorize runs.)

Proposed timeline

Phase Window Notes
Identity foundation Week 1 (first half) Task-multi-email-identity lands before any invite work touches the sign-in path.
Invites Week 1 (second half) Task-workspace-invites-and-roles end-to-end with copy-paste URL.
Invite polish Week 2 (first half) Task-invite-recipient-autocomplete. Can parallelize with audit/soft-delete.
Audit + soft-delete Week 2 (second half) Schema + middleware.
Guardrails Week 3 Rate limits + abuse signals.