Python SDK

Multi-tenancy

Scope every Python Serve request to the authenticated tenant.

The tenant for a request always comes from the authenticated principal. No header, query parameter, body field, or request state can supply or change it.

Where the tenant comes from

By default, Principal.tenant_id scopes the request, and a principal without one is tenant-free. With jwt_strategy, that is the token's org_id claim. Pass tenant= to decide it yourself:

from hypequery.datasets import tenant
from hypequery.serve import Principal, create_api


def tenant_for(principal: Principal):
    org_id = lookup_org(principal.subject)  # your code
    return tenant(org_id) if org_id else None


api = create_api(auth=authenticate, tenant=tenant_for, client=client, datasets={"orders": orders})

A resolver must return a single-tenant scope or None. It cannot grant tenants() with multiple identifiers or all_tenants().

Require a tenant per endpoint

from hypequery.serve import EndpointPolicy, create_dataset_endpoint

endpoint = create_dataset_endpoint(
    dataset=orders, client=client, policy=EndpointPolicy(tenant="required")
)
api = create_api(auth=authenticate, datasets={"orders": endpoint})
tenantBehavior
"required"Tenant-free callers are refused
"optional" (default)The request runs with the caller's tenant, if any
"forbidden"Only tenant-free callers are allowed

When the dataset declares a tenant_key, the query adds the tenant predicate automatically, as in dataset multi-tenancy. A request body that tries to set a tenant is rejected with 400 VALIDATION_ERROR.

In your own routes

Bind the client to the request's tenant before querying:

from hypequery.serve import Call, ServeError, ServeResponse


async def top_countries(call: Call) -> ServeResponse:
    if call.auth.tenant is None:
        raise ServeError(403, "FORBIDDEN", "This report needs a tenant")
    scoped = client.for_tenant(call.auth.tenant)  # an async dataset client
    result = await scoped.execute("orders", {"dimensions": ["country"], "measures": ["revenue"]})
    return ServeResponse.json({"data": list(result.data)})


api.add_route("POST", "/reports/top-countries", top_countries)

A tenant-bound client runs every query as that tenant, and refuses a context naming a different one.

On this page

Get early access to hypequery cloud.

Cloud hosts the same model you run in your backend, with a managed API and MCP server, versions, and rollback. We're letting teams in a few at a time. Join the waitlist and we'll reach out when your project can go live.

How do you want to build your analytics layer? (Optional)
What features do you most need? (Optional)

Pick as many as you like.

Only your email is required. All other questions are optional.