Files
sales-trainer/docs/SAAS_PLAN.md
Macky 056753e8cb feat(saas): multi-tenant isolation (Phase 1) + rate-limit/audit/org-scoped export (Phase 2)
Phase 1 (tenant isolation):
- g.org_id set on require_auth; assert_tenant()/current_org_id() choke-point helpers.
- Multi-org provisioning: POST /api/admin/users {new_org:true} (super_admin) creates a
  new org + its first admin; GET /api/admin/orgs (super_admin sees all, admin own).
- Fixed latent create_org double-id bug (dict id != store key).
- test_saas_tenant.py: org2 admin blocked from org1 group (403), can't list org1
  groups/users, sees only own org; super_admin sees all.

Phase 2 (hardening):
- Rate limit login (per-IP + per-username) + chat send (per-user) to protect LLM cost
  and slow brute force; services/rate_limit.py (in-memory + disk, no external deps).
- Audit log data/audit/audit.jsonl on org.create, user.promote_super_admin, analytics.export.
- CSV export now org-scoped (admin exports only own org).
All 8 backend suites pass.
2026-08-09 09:35:26 +07:00

2.7 KiB

SaaS / Multi-tenant Plan — Sales Trainer

Product is being sold as SaaS. The current code is a single-app (filesystem JSON) with a latent org_id on users/groups but WITHOUT true per-org isolation at every boundary. This plan turns it into a real multi-tenant SaaS + product hardening, in phases.

Current state (verified)

  • org_id exists on: users (users.py:30,78), groups (groups.py:33), created at bootstrap as org-default (factory.py:19).
  • Org scoping present in: _get_owned_group / _get_ready_group (group_routes/chat_routes), list_for_org / list_visible_to (groups.py), list_users(org_id=...) (users.py), admin/me/analytics routes filter by actor.org_id.
  • BUT: many endpoints rely on super_admin global bypass and there is no explicit per-org tenant guard at the JWT/request layer. Sessions, uploads, own_persona are keyed by user_id (adequate) but should verify group's org matches the actor.

Phase 1 — Enforce tenant isolation (multi-tenant correctness) DONE

  1. Tenant context on auth: g.org_id set in require_auth.
  2. Single choke-point guard: assert_tenant() + current_org_id() in helpers; existing _authorize_group org check kept. super_admin is global; admins org-scoped.
  3. Multi-org create: POST /api/admin/users {new_org:true} (super_admin) creates a new org + its first admin; GET /api/admin/orgs platform view (super_admin sees all, admin sees own). Fixed create_org double-id bug.
  4. Regression test test_saas_tenant.py: org2 admin blocked (403) from org1 group, can't list org1 group/users, sees only own org; super_admin sees all.

Phase 2 — Product hardening (provenance, abuse, secrets) DONE

  1. Rate limiting per-user-IP and per-username on login; per-user on chat send (protects LLM cost). New services/rate_limit.py (in-memory + disk, no deps).
  2. Audit log data/audit/audit.jsonl on org.create, user.promote_super_admin, analytics.export.
  3. CSV export org-scoped — admin exports only own org's sessions.

Phase 3 — SaaS-launch readiness

  1. Account/plan model (lightweight): org has plan (trial/pay), seats, active gating login if disabled. Pluggable later.
  2. ToS / legal page + consc ent + privacy acceptance flag on setup.
  3. Signed, expiring tokens for data export; per-org seats enforced on user creation.
  4. Obfuscate persona "recipe" — move internal signal logic server-side so the client never receives full latent recipe (longer-term; the strip in Phase 1 is the interim).

Out of scope for now

  • Real billing/payments, separate storage volumes per tenant, horizontal scale.

Status flags next to each phase item updated as work completes.