Skip to main content

lexigram-tenancy

Multi-tenant resolution, lifecycle, and isolation for the Lexigram Framework.


Overview

lexigram-tenancy provides a composable resolver chain (JWT claim, header, subdomain, path) for tenant identification, ASGI enforcement middleware, three data-isolation strategies (row-level, schema, database), tenant lifecycle CRUD with domain event emission, and per-tenant config overrides — all wired through Lexigram's DI/IoC container.


Full documentation: docs.lexigram.dev

Install

uv add lexigram lexigram-tenancy

# With SQL tenant store
uv add "lexigram-tenancy[sql]"

Quick Start

from lexigram import Application
from lexigram.di.module import Module, module
from lexigram.tenancy import TenancyModule
from lexigram.tenancy.config import ResolutionConfig, TenancyConfig
from lexigram.contracts.tenancy.protocols import TenantProviderProtocol


@module(
    imports=[
        TenancyModule.configure(
            TenancyConfig(
                resolution=ResolutionConfig(
                    resolvers=["jwt_claim", "header"],
                    header_name="x-tenant-id",
                    jwt_claim_key="tenant_id",
                    validator_cache_ttl=300,
                ),
            )
        )
    ]
)
class AppModule(Module):
    pass


async def main() -> None:
    async with Application.boot(modules=[AppModule]) as app:
        provider = await app.container.resolve(TenantProviderProtocol)
        tenants = await provider.list_tenants()
        print(f"Active tenants: {len(tenants)}")


if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

Configuration

Zero-config usage: Call TenancyModule.configure() with no arguments to use all defaults.

Option 1 — YAML file

# application.yaml
tenancy:
  resolution:
    resolvers: ["jwt_claim", "header"]
    header_name: "x-tenant-id"
    jwt_claim_key: "tenant_id"
  lifecycle:
    isolation_strategy: "row_level"
    auto_provision_isolation: true

Option 2 — Profiles + Environment Variables (recommended)

export LEX_TENANCY__ENABLED=true
export LEX_TENANCY__RESOLUTION__RESOLVERS=["jwt_claim", "header"]

Option 3 — Python

from lexigram.tenancy import TenancyModule
from lexigram.tenancy.config import TenancyConfig, ResolutionConfig, LifecycleConfig

TenancyModule.configure(
    TenancyConfig(
        resolution=ResolutionConfig(
            resolvers=["jwt_claim", "header"],
            header_name="x-tenant-id",
            jwt_claim_key="tenant_id",
        ),
        lifecycle=LifecycleConfig(isolation_strategy="schema"),
    )
)

Config reference

Field Default Env var Description
resolution.resolvers ["jwt_claim", "header", "subdomain", "path"] LEX_TENANCY__RESOLUTION__RESOLVERS Ordered resolver list; first match wins
resolution.header_name "x-tenant-id" LEX_TENANCY__RESOLUTION__HEADER_NAME HTTP header read by HeaderTenantResolver
resolution.subdomain_pattern null LEX_TENANCY__RESOLUTION__SUBDOMAIN_PATTERN Base domain for subdomain extraction
resolution.jwt_claim_key "tenant_id" LEX_TENANCY__RESOLUTION__JWT_CLAIM_KEY JWT payload claim key
resolution.validator_cache_ttl 300 LEX_TENANCY__RESOLUTION__VALIDATOR_CACHE_TTL Seconds a validated TenantInfo is cached
lifecycle.isolation_strategy "row_level" LEX_TENANCY__LIFECYCLE__ISOLATION_STRATEGY "row_level", "schema", or "database"
lifecycle.auto_provision_isolation true LEX_TENANCY__LIFECYCLE__AUTO_PROVISION_ISOLATION Run isolation strategy on tenant creation
overrides.cache_ttl 60 LEX_TENANCY__OVERRIDES__CACHE_TTL Seconds a tenant's config dict is cached
integration.cache_key_prefix true LEX_TENANCY__INTEGRATION__CACHE_KEY_PREFIX Prefix cache keys with t:{tenant_id}:
integration.sql_context_bridge true LEX_TENANCY__INTEGRATION__SQL_CONTEXT_BRIDGE Propagate TENANT_ID to lexigram-sql context

Module Factory Methods

Method Description
TenancyModule.configure(config) Configure with explicit TenancyConfig
TenancyModule.stub() Minimal config for testing (exports TenantProviderProtocol and TenantConfigProviderProtocol)

Key Features

  • Resolver chain — JWT claim, header, subdomain, and path resolvers in priority order
  • ASGI middlewareTenantContextMiddleware resolves tenant on every HTTP/WebSocket request
  • Three isolation strategies — row-level (default), schema-per-tenant, database-per-tenant
  • Tenant lifecycle CRUD — create, activate, deactivate, suspend with domain event emission
  • Per-tenant config overrides — key-value overrides with defaults and TenantConfigChanged events
  • Cache key prefixing — wraps CacheBackendProtocol with tenant-prefixed keys automatically
  • lexigram-sql integrationTenantSQLContextBridge enables TenantScope and multi_tenant=True filtering

Testing

import pytest
from lexigram import Application
from lexigram.tenancy import TenancyModule
from lexigram.contracts.tenancy.commands import CreateTenantCommand
from lexigram.tenancy.lifecycle.service import TenantLifecycleService

@pytest.mark.asyncio
async def test_tenant_lifecycle() -> None:
    async with Application.boot(modules=[TenancyModule.stub()]) as app:
        lifecycle = await app.container.resolve(TenantLifecycleService)

        result = await lifecycle.create_tenant(
            CreateTenantCommand(slug="acme", name="ACME Corp")
        )
        assert result.is_ok()
        assert result.unwrap().slug == "acme"

Key Source Files

File What it contains
src/lexigram/tenancy/module.py TenancyModule.configure(), .stub()
src/lexigram/tenancy/config.py TenancyConfig, ResolutionConfig, LifecycleConfig
src/lexigram/tenancy/di/provider.py TenancyProvider bundle and sub-providers
src/lexigram/tenancy/resolution/chain.py CompositeResolver
src/lexigram/tenancy/enforcement/middleware.py TenantContextMiddleware
src/lexigram/tenancy/enforcement/guard.py TenantGuard
src/lexigram/tenancy/lifecycle/service.py TenantLifecycleService
src/lexigram/tenancy/isolation/registry.py IsolationStrategyRegistry
src/lexigram/tenancy/config_overrides/service.py TenantConfigService

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lexigram_tenancy-0.1.3007-py3-none-any.whl (68.4 kB view details)

Uploaded Python 3

File details

Details for the file lexigram_tenancy-0.1.3007-py3-none-any.whl.

File metadata

File hashes

Hashes for lexigram_tenancy-0.1.3007-py3-none-any.whl
Algorithm Hash digest
SHA256 64c611aef18947e2be79a152aabe563d13d4ef6311d07b5c1d14a4c38e0fbff4
MD5 a409dff26cc6edd1d1a1da5c62b34d4f
BLAKE2b-256 caeeb0609c69e4bfe7ad7e165ad68e6e5d26e240e56f3034288207452eef0609

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.5009

1 file

0.1.5007

2 files

0.1.5002

2 files

0.1.5001

2 files

This release

0.1.3007 This release

1 file

0.1.3006

1 file

0.1.3005

1 file

0.1.4

2 files

0.1.2

1 file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page