py-store

Multi-tenant SaaS with per-tenant namespaces

The problem

You run a SaaS product where every customer gets an isolated dataset. The schema is identical for all tenants — the same Order, Customer and Invoice models — but the data must live in a separate database (or schema) per tenant, so that one query can never leak across tenants and a single tenant can be moved, backed up or restored independently.

Duplicating the schema per tenant is unmaintainable, and opening a separate Mongo connection per tenant forces you to thread a connection object through every call site.

Why py-store

Walkthrough

from pymongo import AsyncMongoClient
from py_store import init, store

# One MongoClient serves every tenant database; init maps a connection key -> connection.
client = AsyncMongoClient("mongodb://localhost:27017")
await init({"cluster": client})

# One schema, bound to a namespace. `datasource` picks the connection key,
# `namespace` picks the database inside it.
store.register({
    "name": "Order",
    "collection": "orders",
    "idPrefix": "OD",
    "datasource": "cluster",
    "namespace": "tenant_42",
    "fields": {
        "_id": "string",
        "customerId": {"type": "string", "default": ""},
        "amount": {"type": "float", "default": 0},
        "status": {"type": "string", "default": "open"},
    },
    "computes": {
        "amountLabel": {"type": "string", "depends": ["amount"],
                        "fn": lambda d: f"{d['amount']:.2f}"},
    },
    "indexes": [{"keys": {"customerId": 1, "createdAt": -1}}],
})

# The route override re-targets a query at another tenant without
# re-registering the schema or opening a new client.
# Resolve `tenant` from the authenticated session — never from raw request data.
tenant = "tenant_7"

orders = await store.query(
    "Order($condition:@c0,$sort:@s1,$limit:@l) { _id, customerId, amount, amountLabel }",
    {"c0": {"status": "open"}, "s1": {"createdAt": -1}, "l": 50},
    {"source": "cluster", "namespace": tenant},
)

# Writes take the same override.
await store.insert(
    "Order",
    {"customerId": cust_id, "amount": 199.0},
    {"source": "cluster", "namespace": tenant},
)

Pitfalls

See also