simple_module_tenants
Organisations for simple_module SaaS installs: tenants, many-to-many memberships with per-tenant roles, email-bound invitations, and the tenant resolver that scopes every request.
Install
pip install simple_module_tenants
Add simple_module_tenants to your host's dependencies, then turn on
multi_tenant (Settings screen, or SM_MULTI_TENANT=true) and restart.
Without multi_tenant the module manages organisations but requests are not
scoped to them.
What it does
- Resolution. Registers
app.state.tenant_resolver. The session stores the user's chosen tenant; every request re-validates it against a membership (cached per process, dropped across workers throughInvalidationBus). Suspended tenants resolve to nothing. - Per-tenant roles. A membership role (
owner,admin,member) is added to the principal astenant:<role>for the active tenant only, so tenant roles never reach platform permissions. - Fail-closed handling. With
multi_tenanton, a tenant-scoped query with no tenant raisesTenantIsolationError; this module turns that into a redirect to/tenants(pages) or a403 tenant_required(API).
Usage
| Route | Permission | Purpose |
|---|---|---|
GET /tenants/ |
signed in | My organisations: switch, create |
GET /tenants/members |
tenants.members.view |
Members and invitations of the active tenant |
GET /tenants/invitations/accept?token= |
signed in | Accept an invitation |
GET /admin/tenants/ |
tenants.platform.view |
Platform list of all tenants |
GET/POST /api/tenants/ |
signed in | List mine / create |
POST /api/tenants/{id}/switch |
member of {id} |
Change the active tenant |
/api/tenants/current/members[/{user_id}] |
tenants.members.view / .manage |
List, change role, remove |
DELETE /api/tenants/current/membership |
member | Leave (not the last owner) |
/api/tenants/current/invitations[/{id}] |
tenants.members.manage |
List, invite, revoke |
POST /api/tenants/invitations/accept |
signed in as the invited email | Join |
POST /api/tenants/admin/{id}/suspend · /reactivate |
tenants.platform.manage |
Lifecycle |
Tenant-level routes act on the active tenant (/current), never on an id
from the URL.
Configuration
DB-backed (Settings screen):
allow_self_service(default on) — any signed-in user may create an organisation.invitation_ttl_hours(default 72).subdomain_base(default empty) — withexample.com,acme.example.comresolves to the tenant whose slug isacme. Members get their role there; anonymous visitors and signed-in non-members get the tenant on public routes only; an unknown or suspended subdomain resolves to nothing.public_base_url(default empty) — origin invitation links are built on. Empty makes them root-relative: they are never built from the request'sHostheader, because the same link travels inInvitationCreatedfor a mailer to send.
Management routes need both the permission and an owner/admin role in the
active tenant: a platform-wide grant does not make a plain member of a tenant
its manager (403 tenant_manager_required).
Billing seams
The module ships no billing, but a billing module needs nothing more from it:
app.state.tenants.entitlements— replace the defaultUnlimitedEntitlementswith anEntitlementProvider(limit(tenant_id, key),has_feature(tenant_id, key)). The module enforcestenants.seatson new members and invitations;EntitlementExceededErrormaps to HTTP 402.TenantService.set_status(tenant_id, TenantStatus.SUSPENDED | ACTIVE)for dunning.- Events, published after commit:
TenantCreated,TenantStatusChanged,MembershipAdded,MembershipRemoved,InvitationCreated(also the hook a mailer uses to deliver the invitation link).
Metadata
Release files for simple-module-tenants 0.0.35
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| simple_module_tenants-0.0.35.tar.gz | 39.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| simple_module_tenants-0.0.35-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 89.2 kB
Release files / simple_module_tenants-0.0.35.tar.gz
| Download URL | simple_module_tenants-0.0.35.tar.gz |
|---|---|
| Size | 39.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
27bff8fe0a1661ecacbd9f199e30682977625c9c967a88fdf427c599e77ce478
|
|
BLAKE2b-256 checksum How to use checksums |
3ec3781ffb4dc2f4e059a21c6237566c173751f19d3e57c37adcbc947d025058
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 1, 2026.
Transparency logRelease files / simple_module_tenants-0.0.35-py3-none-any.whl
| Download URL | simple_module_tenants-0.0.35-py3-none-any.whl |
|---|---|
| Size | 49.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b8f0505e88d99ef80f51407c043406b404599364c4f80237a9422c7694366755
|
|
BLAKE2b-256 checksum How to use checksums |
42e8dfb71294411734d7d96a91ec97d015f11575b4ebe609896eba09a0b39482
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 1, 2026.
Transparency log