This release is a pre-release and may not be stable for production use.
TenantShield
Multi-tenant enforcement engine for Python — prevent cross-tenant data leaks by construction, not convention.
The Problem
Multi-tenant SaaS applications can leak data across tenant boundaries
when a single forgotten .filter(tenant_id=...) slips into production.
The leak is silent: queries succeed, responses look normal, and the
breach is often detected only when affected tenants report seeing
data that isn't theirs.
The root cause is structural: tenant scoping is typically enforced by convention (developers remembering to filter), not by the system. One missed filter, one new query path, one ORM method that bypasses the scoped manager — and the boundary is gone.
Before TenantShield
# Django: tenant scoping by convention -- one forgotten filter leaks.
class InvoiceView(View):
def get(self, request):
# Correct: scoped to current tenant
invoices = Invoice.objects.filter(tenant_id=request.tenant.id)
return JsonResponse({"invoices": list(invoices.values())})
def export_all(self, request):
# LEAK: no tenant filter -- returns ALL tenants' invoices.
invoices = Invoice.objects.all()
return JsonResponse({"invoices": list(invoices.values())})
The bug compiles. Tests pass unless they explicitly cover cross-tenant isolation. Production traffic returns wrong-tenant data. No exception is raised.
After TenantShield
# Same model, with TenantShield: scoping is enforced by the system.
from tenantshield.adapters.django import tenant_aware
@tenant_aware
class Invoice(models.Model):
tenant_id = models.CharField(max_length=64)
amount = models.DecimalField(max_digits=10, decimal_places=2)
# ... rest of model
class InvoiceView(View):
def get(self, request):
invoices = Invoice.objects.all() # auto-scoped to request.tenant
return JsonResponse({"invoices": list(invoices.values())})
def export_all(self, request):
# STILL auto-scoped -- no way to forget.
invoices = Invoice.objects.all()
return JsonResponse({"invoices": list(invoices.values())})
Outside a tenant context (e.g., a misconfigured background worker), the
same query raises MissingTenantContextError instead of silently
returning unscoped data. Cross-tenant writes raise
CrossTenantAccessError before the SQL executes.
What was a convention developers had to remember at every call site becomes a constraint the system enforces by default.
TenantShield is a layer over existing ORMs (Django, SQLAlchemy) and request
frameworks (Django, FastAPI, Flask, any ASGI/WSGI application) that enforces
tenant scoping automatically. The default policy is deny-by-default: any
query against a tenant-aware model without an explicit tenant context fails
loudly. Cross-tenant writes are detected and rejected. Tenant context
propagates across async boundaries via Python's native contextvars.
Adoption is incremental — adapters are activated explicitly and there is no import-time monkey-patching.
Status
🔵 Beta — version 0.7.0b0. The architectural arc from foundation
through production hardening + first cohort-validated maturity is
complete (Phases 0 → 6). The Django adapter is validated in production
by a reference adopter (Counterbook, ~196 commits over 6 sprint cycles,
0 architectural findings introduced). SQLAlchemy and DRF adapters are
functional with known limitations documented in the adapters guide;
Celery integration is on the roadmap,
not yet shipped.
Distribution: install from PyPI with pip install --pre tenantshield
(pre-releases require --pre; pip install tenantshield without --pre
will not install beta versions). TestPyPI continues in parallel for
release-candidate testing. A v1.0.0 stable GA release is planned once
a second adapter is validated in production and the Phase 7 architectural
gaps are closed.
Features
Framework adapters
- Django ORM (validated in production by reference adopter, 6 sprint cycles, 0 findings):
tenant_awaremanager + signal enforcement +TenantContextMiddleware. - Django REST Framework (functional; test coverage expanding): triple defense (permissions + viewset mixin + serializer validation).
- SQLAlchemy 2.x sync + async (functional; Phase 7 gaps documented —
_unsafe_unscopedandauto_propagate_from_parent_fkparity with Django pending; see Known Limitations):@tenant_awaredeclarative decorator + event-based write enforcement (before_insert/before_update/before_delete) + read filtering viado_orm_execute+ scope managers (SessionScope/AsyncSessionScope). - ASGI middleware:
TenantSessionMiddleware(sync ctx mgr with dual-mode resolver) +AsyncTenantSessionMiddleware(async-native; Phase 5A) for FastAPI / Starlette / any ASGI 3.0 framework. - WSGI middleware:
TenantSessionMiddlewareWSGIwith generator-safe scope (Flask / Django WSGI / Gunicorn).
Core capabilities
- Tenant scope contracts:
tenant_scope(ctx)+atenant_scope(ctx)for explicit binding;bind_session_to_tenant/bind_async_session_to_tenantfor SA session binding. - Policy engine:
DenyByDefaultPolicy,AllowListPolicy,RequireScope,ChainPolicy. Composable. - Audit bus: pluggable sinks (
StructLogSink,InMemorySink,NullSink, or custom). 6AuditEventTypevalues (CONTEXT_BOUND/CONTEXT_RELEASED/POLICY_ALLOW/POLICY_DENY/ENFORCEMENT_VIOLATION/SINK_FAILURE). - Cross-adapter extraction strategies:
HeaderStrategy,HostStrategy,JWTStrategy,CallableStrategyoperating on a minimalRequestProtocolabstraction. Same strategy instance works across Django + ASGI.
Production hardening (Phase 5)
- Structured observability:
tenantshield.observabilitymodule with a 9-event taxonomy across scope lifecycle + enforcement + middleware boundaries. structlog-based emission with~6 ns/calldisabled-default gate. - Audit-observability dual-pattern: policy-level audit events + operation-
level observability events emit at independent gates (sink registry vs
is_enabled()flag). Decision 7-A separation verified empirically. - Adopter-extensible processor chain: OpenTelemetry trace context + Prometheus metrics integrate as adopter-prepended structlog processors; zero TenantShield-side coupling.
Install
TenantShield is published to public PyPI as a beta pre-release. Use the
--pre flag because pip does not install pre-releases by default:
pip install --pre tenantshield
Adapter-specific extras:
pip install --pre "tenantshield[django]" # Django + DRF adapter
pip install --pre "tenantshield[sqlalchemy]" # SQLAlchemy adapter
pip install --pre "tenantshield[jwt]" # JWT strategy support
pip install --pre "tenantshield[drf]" # Django REST Framework adapter
pip install --pre "tenantshield[all]" # all adapters at once
TestPyPI continues in parallel for release-candidate testing:
pip install --pre --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ tenantshield
Once v1.0.0 ships, pip install tenantshield (without --pre) will
prefer the stable release automatically.
Quickstart
The canonical minimum: register an audit sink, define a policy, enter a tenant scope, and evaluate operations.
from tenantshield import (
DenyByDefaultPolicy,
Operation,
OperationType,
StructLogSink,
TenantId,
bind_tenant,
evaluate_and_audit,
register_sink,
tenant_scope,
)
# 1. Register a sink so audit events go somewhere.
register_sink(StructLogSink())
# 2. Define a policy.
policy = DenyByDefaultPolicy()
# 3. Enter a tenant scope.
ctx = bind_tenant(TenantId("acme"))
with tenant_scope(ctx):
# 4. Evaluate an operation.
operation = Operation(
model="app.Invoice",
operation_type=OperationType.READ,
tenant_context=ctx,
)
decision = evaluate_and_audit(policy, operation)
print(decision) # Allow()
Outside a tenant scope, the same evaluation returns
Deny(reason="No tenant context active for read on 'app.Invoice'").
For framework-specific quickstarts (Django, SQLAlchemy + FastAPI, SQLAlchemy + Flask), see the Adapters documentation and the Examples below.
Enable observability (Phase 5, opt-in)
from tenantshield.observability import configure
configure(emit_events=True)
Disabled by default — the gate adds ~6 ns/call when off and zero log
volume. See Observability Quick Start
for adopter integration patterns.
Documentation
- Getting Started — install + minimal example.
- Concepts — building blocks (TenantContext, policies, audit bus, registry).
- API Reference — complete public surface.
- Adapters — Django + SQLAlchemy + middleware integration guides.
- Observability:
- Quick Start — enable emission + configure structlog.
- Dual-Pattern — audit bus + observability semantics.
- Async Middleware Migration
- Production Checklist
- OpenTelemetry integration
- Prometheus integration
- Architectural Decision Records — 14 ADRs documenting design decisions.
- Changelog — release history with detailed Decision Records per phase.
Examples
Runnable adopter examples lives in examples/:
- FastAPI + SQLAlchemy — async ASGI
application with
TenantSessionMiddleware. - Flask + SQLAlchemy — sync WSGI
application with
TenantSessionMiddlewareWSGI. - CLI + SQLAlchemy — framework-agnostic
background-worker pattern with
SessionScope. - Django adopter starter — Django project template wired with TenantShield middleware + admin.
Compatibility
| Versions | |
|---|---|
| Python | 3.11, 3.12, 3.13 |
| Django | 4.2 LTS, 5.x |
| SQLAlchemy | 2.x (sync + async) |
| FastAPI | >=0.115 |
| Flask | recent versions (WSGI standard) |
structlog>=25.0,<26.0 is the only required runtime dependency.
Architectural maturity
- Phases shipped: 0 (foundation) → 1 (core API) → 2 (Django + DRF) → 3 (SQLAlchemy sync) → 4 (SQLAlchemy async + cross-adapter strategies) → 5 (production hardening).
- 557 tests (541 library + 16 example) with 99.63% library coverage.
- 12 ADRs + 43 active Decision Records + 73 normative Rules.
- 0 architectural BLOCKERs in Phase 5 (best Phase profile sustained from Phase 4).
See the Documentation for comprehensive guides, API reference, and architectural decisions.
License
Released under the Apache License 2.0.
Metadata
Release files for tenantshield 0.7.0b2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tenantshield-0.7.0b2.tar.gz | 358.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tenantshield-0.7.0b2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 459.5 kB
Release files / tenantshield-0.7.0b2.tar.gz
| Download URL | tenantshield-0.7.0b2.tar.gz |
|---|---|
| Size | 358.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f6d1c5c8c637e83a189211c5e39f71601ab73848a2c39a1ef212172e4085b40e
|
|
BLAKE2b-256 checksum How to use checksums |
c24709c692be58a1ca40f9474a8ecb4275b0df5454c06ccaaf0f429a8c13abd7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 16, 2026.
Transparency logRelease files / tenantshield-0.7.0b2-py3-none-any.whl
| Download URL | tenantshield-0.7.0b2-py3-none-any.whl |
|---|---|
| Size | 101.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b7b4e66924413ffc43f4b46e42f5834132a6a7ecdb905579f30bd3ee3a1e0ebb
|
|
BLAKE2b-256 checksum How to use checksums |
4b82b2656b6a0d293bae3a76e3a407a90e444fa45eccfb6e68eaa2ff40c4410d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 16, 2026.
Transparency log