Skip to content

Multi Tenancy Progress

Ed Mozley edited this page Jul 15, 2026 · 14 revisions

Multi-tenancy: progress tracker

Part of the Multi-tenancy section. Planned / in active development. Tracks the build against the plan. Last updated: 2026-07-05.

Legend: βœ… done Β Β·Β  🚧 in progress / next Β Β·Β  ⬜ not started

Where things stand: the tickets module is now solid for real multi-company use. The foundation is complete; tickets scope by company; per-company email routing works end-to-end (pinned/shared mailboxes, sender-domain and individual-address routing, a triage safety net, and diagnostics); the first per-company settings are live (ticket types & origins); the whole ticket layer is fully isolated (every ticket read/write is access-checked β€” see Data isolation & the audit); and you can now control which companies each analyst may access. What's left is rolling per-company scoping through the other modules, plus per-company departments/SLAs and the self-service portal.


Phase 0 β€” Foundation βœ… complete

The invisible plumbing. None of this changes anything on a single-company install.

  • βœ… tenants table + a silent Default company that owns all existing data
  • βœ… tenant_domains table (for future email-domain routing)
  • βœ… Analyst β†’ company access model (which staff can see which companies, + an "all companies" flag) β€” now with an admin UI to grant/deny it per analyst (System β†’ Analysts; see Staff access)
  • βœ… Team β†’ company access β€” a team can grant its members access to specific companies (or all), additive to each analyst's own access, set on System β†’ Teams β†’ Manage companies. Enforced through the same getAccessibleTenantIds() choke-point; defaults to granting nothing so upgrades never widen access. System β†’ Analysts shows effective (direct + team-inherited) access. New table team_tenant_access + teams.can_access_all_tenants.
  • βœ… tenant_id on tickets (every existing ticket assigned to the Default company)
  • βœ… Shared tenancy helper β€” the single choke-point all tenant logic routes through
  • βœ… Active-company session context
  • βœ… Company switcher in the header (appears only once a 2nd company exists)
  • βœ… System β†’ Companies admin page (list / create / rename companies)

Phase 1 β€” Tickets βœ… core complete

Making the tickets module company-aware.

  • βœ… Ticket list scopes to the active company
  • βœ… New tickets are stamped with the active company
  • βœ… Folder count badges (departments / statuses / analysts / unassigned) scope to the active company
  • βœ… Full ticket-layer isolation (Phase 1.5) β€” every ticket endpoint (detail, thread, attachments, notes, time, audit, CMDB, SLA, CSAT, and every action) is now access-checked, verified by a three-sweep audit. The tickets module is safe for real multi-company use.

Phase 2 β€” Per-company mailboxes & email routing βœ… core complete

So an MSP can receive each client's email and have it land under the right company.

  • βœ… Link mailboxes to companies (pinned = one mailbox per company, shared intake = one inbox sorted by sender) β€” set in Tickets β†’ Settings β†’ Mailboxes
  • βœ… Register each company's email domains β€” managed in System β†’ Companies
  • βœ… Route incoming email to the right company by sender domain (covers both Microsoft & Gmail import)
  • βœ… Specific senders β€” map an individual address to a company (e.g. jane@gmail.com β†’ Acme), matched before the domain, so personal/webmail senders route correctly
  • βœ… Triage queue for unmatched / unknown senders β€” reached from the company switcher, with one-click filing and "route this domain" or "always file this sender" (each sweeps the other queued mail and auto-routes future mail)
  • βœ… Public-email (Gmail/Outlook/…) domains are never auto-mapped (different clients share them); the built-in list is customisable (add-only β€” extend it, can't un-protect a built-in)
  • βœ… "How email reaches this company" β€” a read-only routing summary on each company's editor
  • βœ… Email routing test β€” a dry-run diagnostic: type a sender + mailbox, see where it would land and why
  • ⬜ A per-company reply-identity override (replies already go out from the mailbox the ticket arrived on; this is the only outbound piece left) (next up)

Routing rule (first match wins): reply to an existing ticket β†’ inherits its company; otherwise a pinned mailbox β†’ its company (sender ignored); otherwise a shared mailbox β†’ matched by the exact sender address, then the sender domain β†’ that company, or triage if nothing matches. On a single-company install this is invisible β€” everything lands under the one Default company exactly as before.

Phase 3+ β€” Roll scoping through the other modules 🚧 started

  • 🚧 Per-module Settings: global default + per-company override (the add + hide model). Built: ticket types, ticket origins. Next: categories, departments (with extra wiring), SLAs. Statuses/priorities/teams/mailboxes stay global on purpose β€” see Settings rationale.
  • βœ… Change Management β€” company-scoped like tickets/problems (list + calendar scope to the active company; new changes stamped with it; every by-id read/write, UI and REST API, isolated by company; a change in one company is a 404 to another). changes.tenant_id, NULL = Default; invisible at N=1.
  • βœ… Asset Management β€” assets belong to a company (assets.tenant_id); the list and dashboard scope to it, agent ingest files machines by their API key's company (apikeys.tenant_id, so two clients can each have a LAPTOP-01), and every by-id read/write (incl. an asset's disks/software/history child reads), UI and REST API, is isolated. Asset types, statuses are per-company add/hide lists, and locations are a shared+own tree. Suppliers stay a shared registry (used by Contracts/RFP too β€” changeable later). Deferred: routing vCenter/Intune-synced machines to their company (they land in Default for now). See the Developer Guide β€” assets is its worked example.
  • ⬜ CMDB, Calendar, Forms, Network Mapper, then Knowledge (two-axis global/per-company)

Self-service portal 🚧 SSO built; ticket isolation still pending

  • ⬜ Global requester identity β€” one login, tickets across companies, each badged
  • ⬜ Portal scoping + the "only your own tickets" guardrails
  • βœ… Per-company SSO β€” requesters sign in via their own company's identity provider, routed by email domain (0 / 1 / 2+ IdPs β†’ local / straight-through / picker); single-company and MSP both supported, invisible at N=1. Uses each provider's own credentials today; the credential-free "MSP multi-tenant app + consent" model is designed but not built. See Single Sign-On.

Cross-cutting / housekeeping

  • βœ… README β€” an "in development" multi-tenancy section is now in the project README
  • βœ… Admin visibility tools β€” System β†’ Topology (a company-rooted tree of mailboxes, domains, providers, analysts, ticket/requester counts across every company) and System β†’ Orphaned tickets (find + rehome tickets stuck in a deleted department, which are hidden from every team queue). See System.
  • ⬜ Isolation beyond the tickets module β€” other modules that read tickets (self-service portal, CMDBβ†’tickets, Tasks↔tickets, Watchtower, the SLA cron, the workflow engine, templated/CSAT email) are not yet company-scoped; only matters at N>1, fix as each module gets its Phase-3 scoping. See Data isolation.
  • ⬜ Companies: delete / archive management (today you can only deactivate)
  • ⬜ Automated isolation tests (guarding against one company seeing another's data)
  • ⬜ Tidy-up: remove the api/tickets/debug_get_attachment.php diagnostic (discloses server paths)

This page is maintained by hand as work lands β€” see the GitHub roadmap issue (#24) for live discussion.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally