--- kind: epic slug: tenant-lifecycle title: Tenant lifecycle — invites, roles, soft-delete, audit, rate-limits plan_slug: multitenant-saas-hardening status: ready priority: P1 tenant_id: global cursor_epic_id: null updated_at: "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). ## 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 - [x] An owner can invite an email; the recipient lands in the workspace after sign-in. - [x] 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). - [x] 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.) - [x] 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. |