Skip to content

feat(tenancy): route each signed-in request to its tenant's schema (M2b, #93) - #100

Merged
amar-python merged 1 commit into
mainfrom
feat/m2b-tenant-schema-routing
Oct 26, 2026
Merged

amar-python merged 1 commit into
mainfrom
feat/m2b-tenant-schema-routing

Conversation

@amar-python

Copy link
Copy Markdown
Owner

Fixes #93. This is part 2 of ART Milestone 2 (BR-36; decision 6: schema per tenant).

Behaviour

Request Runs in Auth
Authorization: Bearer <session> schema tenant_<slug> (registry, audit log, uploaded tables) session (#92)
No Authorization header the deployment's default schemas, as today existing API-key gate
Invalid or expired bearer — 401, never a fallback to the default schemas

What this means for each tenant:

  • Other tenants' data is out of reach. A tenant cannot list, read rows from, or delete another tenant's files or tables. Requests without a session see no tenant data.
  • Duplicate detection is per tenant. The registry's UNIQUE constraints now apply inside each tenant's schema, so the same file uploaded by two tenants is not a duplicate. This is part of BR-28.
  • The audit log is per tenant.
  • T&E mode under a tenant session returns 409, with a message pointing to M2b-2: per-tenant T&E tables (BR-36) #99 (per-tenant T&E tables, your 26 Oct decision). This keeps tenants out of the shared T&E schema until their own exists.

How

  • api/tenancy.py. The tenant schema is held in a context variable. Services read it through uploads_schema() and te_schema(); the 23 direct reads of settings.UPLOADS_SCHEMA and settings.TE_SCHEMA in services and routers now go through these functions.
  • tenant_scope replaces require_api_key on the csv, T&E and audit routers. It's async on purpose: only an async dependency sets the variable in the context that the endpoint's worker thread copies.
  • Tenant schemas get the per-tenant migrations. 0004 (the shared control schema) is skipped for tenant runs. A tenant's schema is migrated when scripts/create_tenant_admin.py creates it, and every existing tenant is brought to head at startup ("migrations run across N schemas", decision 6). Migrating a tenant swaps a setting under a lock, so it's restricted to startup and the CLI, never a request.

Evidence

All runs are against local PostgreSQL 16.

Check Result
Local replica of the CI integration job (scripts/test_report.py --strict) 277 passed, 0 failed, 0 skipped (268 on main + 9 new)
Evals p,i,s,e 26/26
tests/test_tenant_isolation.py 9 passed, three runs in a row
Same tests with tenant routing removed from tenant_scope 6 of 8 fail (as expected; the concurrency test came later)
Concurrency 60 requests across 4 threads (tenant A, tenant B, no session): each saw only its own data
DB-free pytest 126 passed
scripts/lint.sh clean
CLI end to end tenant created → schema tenant_<slug> exists → its admin signs in, uploads, and sees only that file

Not in this PR


Generated by Claude Code

…2b, #93)

A request with a bearer session runs in schema tenant_<slug>: its file
registry, audit log and uploaded tables live there, so another tenant
cannot list, read, delete or collide with them, and duplicate detection
becomes per tenant. Requests without a session keep the API-key gate and
the default schemas. An invalid session is 401, never a fallback.

The schema is a context variable set by the async tenant_scope
dependency; services read it through uploads_schema()/te_schema().
Tenant schemas get the per-tenant migrations (0004 is shared-only) when
created by scripts/create_tenant_admin.py and at every startup.
T&E mode for a tenant returns 409 until per-tenant T&E tables (#99).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YPoXzBSj89ZHgw7nfTmbB
@amar-python amar-python added this to the ART extension milestone Oct 26, 2026 — with Claude
@amar-python amar-python added enhancement New feature or request art ART framework extension work labels Oct 26, 2026 — with Claude
@amar-python
amar-python merged commit 54a20e2 into main Oct 26, 2026
3 checks passed
@amar-python
amar-python deleted the feat/m2b-tenant-schema-routing branch October 27, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

art ART framework extension work enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M2b: route every request to its tenant's schema (BR-36)

2 participants