71 lines
3.2 KiB
Markdown
71 lines
3.2 KiB
Markdown
|
|
---
|
||
|
|
kind: plan
|
||
|
|
slug: multitenant-saas-hardening
|
||
|
|
title: Multitenant SaaS hardening — invites, audit, guardrails, test foundation
|
||
|
|
status: ready
|
||
|
|
priority: P1
|
||
|
|
tenant_id: global
|
||
|
|
cursor_plan_id: null
|
||
|
|
updated_at: "2026-06-01"
|
||
|
|
---
|
||
|
|
|
||
|
|
# Plan overview
|
||
|
|
|
||
|
|
`Plan-multitenant-cursor-sync` covers the *Cursor sync layer* slice of multitenancy. This plan covers the rest: the operational hardening needed before Echodo can be safely used as a real multitenant SaaS rather than a homelab single-tenant app.
|
||
|
|
|
||
|
|
The unifying theme is **tenant blast-radius** — what's the worst thing a tenant (or a bug operating on behalf of a tenant) can do, and how do we cap it? Today the answer is "we don't really know," because there's no invite flow, no audit log, no rate limit, and no test runner.
|
||
|
|
|
||
|
|
## Objectives and success criteria
|
||
|
|
|
||
|
|
- A workspace owner can invite, role-change, and remove members from the app UI.
|
||
|
|
- Every mutation against a tenant-scoped table is recorded in an append-only audit log with actor user id, action, and target.
|
||
|
|
- Workspaces support soft-delete (archive) without losing data, and restore.
|
||
|
|
- Public mutation endpoints have rate limits that survive a brute-force attempt against credentials sign-in.
|
||
|
|
- `pnpm test` runs Vitest across the workspaces that have tests, and a minimal GitHub Actions CI runs lint + type-check + test on PRs.
|
||
|
|
|
||
|
|
## Scope and boundaries
|
||
|
|
|
||
|
|
**In scope**
|
||
|
|
|
||
|
|
- Invite + role management (DB + UI + tRPC).
|
||
|
|
- Audit log table, write path, minimal read view.
|
||
|
|
- Workspace soft-delete and restore.
|
||
|
|
- Rate-limiting middleware on critical mutations.
|
||
|
|
- Vitest bootstrap + GitHub Actions CI.
|
||
|
|
|
||
|
|
**Out of scope (this plan)**
|
||
|
|
|
||
|
|
- Billing / Stripe (a separate plan when there's actual revenue to charge for).
|
||
|
|
- SSO group-based role mapping (Authentik groups → workspace roles). Defer until manual invites are in.
|
||
|
|
- Full SOC2 audit posture. The audit log here is a foundation, not a compliance artifact.
|
||
|
|
|
||
|
|
## Cross-team collaborators
|
||
|
|
|
||
|
|
| Name / group | Role |
|
||
|
|
|--------------|------|
|
||
|
|
| Solo operator (you) | Everything. |
|
||
|
|
|
||
|
|
## Epics
|
||
|
|
|
||
|
|
| Epic | Link |
|
||
|
|
|------|------|
|
||
|
|
| Tenant lifecycle | `./Epic-tenant-lifecycle/Epic-tenant-lifecycle.md` |
|
||
|
|
| Test foundation | `./Epic-test-foundation/Epic-test-foundation.md` |
|
||
|
|
|
||
|
|
## Metrics and milestones
|
||
|
|
|
||
|
|
| Milestone | Target | Metric |
|
||
|
|
|-----------|--------|--------|
|
||
|
|
| Invites shipped | M1 | Owner can invite a new email and the recipient can accept and land in the workspace. |
|
||
|
|
| Audit visibility | M2 | Every mutation on `objects` / `workspaces` / `workspace_members` produces an audit row with `actor_user_id`. |
|
||
|
|
| Guardrails live | M3 | Brute-forcing credentials sign-in is rate-limited at the route handler. |
|
||
|
|
| CI green on PRs | M4 | A PR with `pnpm lint && pnpm type-check && pnpm test` failing cannot merge. |
|
||
|
|
|
||
|
|
## Risks and mitigations
|
||
|
|
|
||
|
|
| Risk | Mitigation |
|
||
|
|
|------|------------|
|
||
|
|
| Soft-delete semantics get confused with hard-delete | One column (`archived_at` timestamp); every workspace-scoped query adds `archived_at IS NULL` by default. No new "is_deleted" boolean. |
|
||
|
|
| Audit log becomes a write-amplification problem | Single append-only table with a partial index on `workspace_id, created_at`. Don't denormalize until you measure. |
|
||
|
|
| Vitest adoption stalls | Start with `packages/database` and `packages/shared` only. Don't try to test React components in v1. |
|