# iurban-tenancy

Shared multi-tenancy data layer for iUrban microapps (ADR-006). Stops every microapp from copying
`tenancy.py` / reinventing RLS.

## What it gives you

- `Base` + `metadata` with a stable Alembic **naming convention** (reproducible migrations).
- Column mixins: `UuidPkMixin`, `TimestampMixin`, `TenantMixin` (`tenant_id`, indexed, not-null).
- `apply_tenant(session, tenant_id)` — the RLS helper (`SET LOCAL app.current_tenant`, **inside a txn**).
- `rls_ddl(table)` — SQL to enable+force RLS and add the tenant-isolation policy, for migrations.
- `iurban_tenancy.fastapi.make_tenant_session(sessionmaker, get_tenant_id)` — a tenant-scoped
  session dependency (decoupled from auth; the app wires `get_tenant_id` from `iurban-auth`).

## Use

```python
from iurban_tenancy import Base, UuidPkMixin, TenantMixin, TimestampMixin, rls_ddl

class Incident(UuidPkMixin, TenantMixin, TimestampMixin, Base):
    __tablename__ = "incidents"
    # ... your columns

# migration: op.execute(rls_ddl("incidents"))
```

```python
# app wiring (FastAPI + iurban-auth)
from fastapi import Depends, HTTPException
from iurban_auth import current_user
from iurban_tenancy.fastapi import make_tenant_session
from .db import async_session

def tenant_id(user = Depends(current_user)) -> str:
    if not user.tenant_id:
        raise HTTPException(403, "token carries no tenant_id")
    return user.tenant_id

tenant_session = make_tenant_session(async_session, tenant_id)
```

## Rules (ADR-006)

App connects as a **non-superuser** role; `apply_tenant` runs **inside a transaction** (safe with
PgBouncer transaction-pooling — a non-`LOCAL` `SET` would leak across tenants). Schema changes only
via Alembic migrations.
