Architecture
Multi-tenancy
Multi-tenancy is the most important invariant in the codebase. Data leakage across workspace boundaries is a security incident, not a bug. The system enforces separation through layered mechanisms: traits, global scopes, policies, middleware, and a dedicated regression test.
Overview
Every tenant-scoped Eloquent model carries one of two traits. Both apply a global scope to all queries and auto-fill the workspace id at creation time, so accidental cross-workspace inserts are blocked at the ORM layer rather than relying on developers remembering to filter.
The trait pattern
BelongsToWorkspace— for models with a directworkspace_idcolumn (agents, sources, leads, usage events).BelongsToAgent— for models that hang off an agent (documents, chunks, conversations, messages). The trait resolves the workspace through the agent relationship.
Both traits register a global scope that adds where workspace_id = ? to every query, and a creating hook that auto-fills the column from the resolved current workspace.
Workspace resolution
The current workspace is determined once per request and frozen in a request-scoped resolver:
| Request type | Workspace source |
|---|---|
| Authenticated dashboard request | Read from users.default_workspace_id; membership in workspace_users is verified. |
| Widget request | Derived from the agent_id claim inside the signed JWT; never trusted from request params. |
| System / queued job | Set explicitly when the job is dispatched and restored in the job handler. |
The CurrentWorkspace resolver clears state at the end of each request, which matters under Octane because workers are long-lived.
Tenant vs cross-workspace tables
Tenant-scoped (direct or via agent): agents, agent_versions, sources, documents, chunks, conversations, messages, leads, curated_answers, usage_events, knowledge_gaps.
Cross-workspace (no scoping): users (a user may belong to many workspaces), workspaces, plans, and the standard Laravel infrastructure tables (jobs, failed_jobs, cache, sessions).
Vector store isolation
Every vector written to Vectorize or Qdrant carries agent_id and workspace_id in its metadata. Queries filter by agent_id, using the vector store's native metadata filter — there is no application-level post-filter that could be bypassed.
Testing & enforcement
MultiTenancyTest creates overlapping data in two workspaces and asserts that no query in either workspace surfaces the other's rows. It also reflects every Eloquent model to confirm that any model with a workspace_id column declares the matching trait. The test is part of the required CI suite.
Note. The only legitimate way to opt out of the global scope is the platform-admin context, which uses withoutGlobalScope explicitly with a comment justifying the cross-workspace read.