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>
4.3 KiB
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_membersalready exists; add invite flow on top). - Smart invite-recipient autocomplete (members / pending / known users / new email).
- Append-only
audit_logtable 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).
Related tasks
| 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
workspacesandinvitesproduces anaudit_logrow. 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
authorizecallback. (SeeTask-rate-limit-and-abuse-guardrailsfor the deviation rationale — Auth.js v5 doesn't expose a route-handler hook beforeauthorizeruns.)
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. |