Skip to content
GitHubDiscord

Multi-Tenancy

Lexigram provides a first-class Multi-Tenancy engine (lexigram-tenancy) designed to scale from simple shared databases to complex, high-security multi-regional deployments.


Multi-tenancy in Lexigram revolves around three pillars:

  1. Resolution: Identifying which tenant a request belongs to.
  2. Isolation: Separating data at rest (Schema, Table, or Database).
  3. Enforcement: Ensuring the current security context is bound to the resolved tenant.

Resolution happens at the edge of the request pipeline. Lexigram supports several strategies:

StrategySourceUse Case
headerX-Tenant-IDAPI Integrations, Mobile Apps
jwt_claimsub or tidOAuth2 / OIDC authenticated requests
subdomaintenant.app.comTraditional SaaS platforms
path/api/v1/:tenant/...Multi-organization public portals
application.yaml
tenancy:
resolution:
strategy: subdomain
domain: lexigram.dev

Lexigram can isolate data in three ways, depending on your scalability and compliance needs.

All tenants share the same table; every row has a tenant_id column.

  • Pros: Simplest to manage, cheapest to run.
  • Cons: Highest risk of data leakage if queries are not properly filtered.

B. Schema Isolation (Same DB, Different Schema)

Section titled “B. Schema Isolation (Same DB, Different Schema)”

Each tenant has its own Postgres schema (e.g., tenant_a.users, tenant_b.users).

  • Pros: Stronger isolation, easy migrations.
  • Cons: Moderate management overhead.

Each tenant has a physically separate database instance.

  • Pros: Maximum security and performance isolation. Required for some compliance standards (SOC2/HIPAA).
  • Cons: Most complex to manage and scale.

Once resolved, the TenantContext is available throughout the DI container. You don’t need to pass tenant_id manually to your functions.

from lexigram.tenancy import TenantContext
class DocumentService:
def __init__(self, context: TenantContext) -> None:
self.context = context
async def list_documents(self):
# The context automatically knows the current tenant ID
print(f"Listing docs for tenant: {self.context.tenant_id}")
...

The TenancyGuard ensures that if a route is marked as tenant-scoped, a valid tenant must be resolved, or the request is rejected with a 401 Unauthorized (if missing) or 403 Forbidden (if the user doesn’t belong to that tenant).

from lexigram.tenancy.enforcement import tenant_scoped
from lexigram.web import Controller, get
@tenant_scoped()
class OrganizationController(Controller):
@get("/settings")
async def get_settings(self):
# This route is only accessible if a tenant is resolved
...

[!CAUTION] When using Row-Level Isolation, ensure you use Lexigram’s built-in Query Interceptors. These automatically inject WHERE tenant_id = :current_tenant into every SQL statement, preventing accidental cross-tenant data exposure.