---
audience: developers
summary: How tenants authenticate, what limits apply, where to inspect usage.
title: Auth + quotas
path: concepts/auth-and-quotas
status: published
---


# Auth + quotas

ScaiScribe authenticates every request via [ScaiKey](https://scailabs.ai/docs/scaikey), the ScaiLabs identity service. Tenants get limits per the quota model. Operators get aggregated rollups so they can see usage at the partner and tenant level.

## Authentication

There are two token types you'll meet:

### 1. Service tokens (the default for SDK / programmatic use)

OAuth `client_credentials` against ScaiKey's `/oauth/token` endpoint. The SDKs fetch and refresh these transparently:

```python
from scaiscribe import Client, ScaiKeyAuth

auth = ScaiKeyAuth(
    client_id=os.environ["SCAIKEY_CLIENT_ID"],
    client_secret=os.environ["SCAIKEY_CLIENT_SECRET"],
)
client = Client(
    "https://scaiscribe.scailabs.ai",
    auth=auth,
    default_tenant_id=os.environ["SCAISCRIBE_TENANT_ID"],   # required
)
```

Service tokens are issued globally to the application registered in ScaiKey — they don't carry a `tenant_id` claim by themselves. You tell ScaiScribe which tenant the request is acting on via the `X-ScaiScribe-Tenant-Id` header, which the SDK sends automatically when you pass `default_tenant_id` (Python) / `defaultTenantId` (TS/.NET) at construction time.

### 2. User tokens (browser / OIDC flows)

When a user authenticates through ScaiKey's web flow, the resulting JWT *does* carry a `tenant_id` claim — the tenant the user belongs to. ScaiScribe trusts the JWT claim; the `X-ScaiScribe-Tenant-Id` header is ignored for these tokens (defence-in-depth — a user token can't be re-targeted to a different tenant).

The admin SPA at `/admin/` uses this flow: backend-mediated Authorization Code with HttpOnly session cookies.

### Defence-in-depth boundary

The `X-ScaiScribe-Tenant-Id` header only takes effect when the underlying token has *no* tenant claim of its own. If the token already declares a tenant, that wins — full stop.

## Quotas

Five axes are tracked per tenant. v1.0 defaults are:

| Axis | Default | Type | HTTP code on violation |
|---|---|---|---|
| `documents_count` | 10,000 | Absolute (live count from `documents`) | 403 `QUOTA_EXCEEDED` |
| `assets_bytes` | 5 GB | Absolute (SUM over `assets` + `fonts`) | 403 `QUOTA_EXCEEDED` |
| `templates_count` | 20 | Absolute (live count from `templates`) | 403 `QUOTA_EXCEEDED` |
| `renders_per_day` | 1,000 | Windowed (per UTC day, rolls over at midnight) | 429 `QUOTA_EXCEEDED` |
| `max_doc_size_mb` | 100 | Per-document size cap | 413 |

Absolute quotas use a 403 because the situation only resolves with cleanup (delete documents, remove assets). Windowed quotas use a 429 because retrying later might succeed — the bucket rolls over naturally.

Quota overrides per tenant land via a forthcoming `tenant_quotas` table; until then the defaults apply uniformly. Operator override requires a code change.

## Inspecting usage

Three admin endpoints help operators see what's happening. Access varies by role:

### Per-partner rollup

```
GET /v1/admin/quotas/partner/{partner_id}?axis=renders_per_day
```

Returns the aggregate across every tenant of a partner. Useful for provisioning reviews and capacity planning. Access: **superadmin** OR **partner_admin** on the queried partner.

```json
{
  "partner_scaikey_id": "prt_acme",
  "axis": "renders_per_day",
  "tenant_count": 12,
  "total_used": 4823,
  "total_limit": 12000,
  "per_tenant": [
    { "tenant_id": "tnt_alpha", "used": 1450, "limit": 1000 },
    { "tenant_id": "tnt_beta",  "used":  220, "limit": 1000 },
    …
  ]
}
```

### Per-user rollup

```
GET /v1/admin/quotas/tenant/{tenant_id}/users?axis=renders_per_day
```

Breaks a tenant's usage down by acting user. Access: **superadmin** OR **partner_admin** on the tenant's partner OR **tenant_admin** on the tenant.

```json
{
  "tenant_id": "tnt_acme",
  "axis": "renders_per_day",
  "total_used": 217,
  "coverage": "full",
  "per_user": [
    { "user_scaikey_id": "usr_alice", "used": 142, "limit": 1000 },
    { "user_scaikey_id": "usr_bob",   "used":  75, "limit": 1000 }
  ]
}
```

The `coverage` field reports whether per-user attribution is available for the axis:
- **`full`** — for `renders_per_day` and `documents_count`. Solid attribution.
- **`none`** — for `assets_bytes` and `templates_count`. The Asset/Font/Template tables don't carry `created_by` yet (model gap; v1.x). `per_user` is empty in this case and a `note` field explains.

### Admin UI

The admin SPA exposes these inline on the Partners and Tenants pages (click "Quota" / "Per-user" on a row to expand), and as a dedicated `/admin/quotas` page with axis switching.

## Roles

| Role | Scope | Powers |
|---|---|---|
| `member` | One tenant | Standard tenant user — can use the document APIs as their tenant. |
| `tenant_admin` | One tenant | Above + manage that tenant's users, see its quota rollups. |
| `partner_admin` | One partner | Above + cross every tenant of that partner. |
| `superadmin` | Global | Above + manage every partner, rotate secrets, see global health. |

Roles are assigned via `POST /v1/admin/users/{user_id}/roles` (audit-logged with `granted_by_scaikey_id`). Revocation stamps `revoked_at`; rows stay for audit.

## Where to next

- [Quickstart](/docs/scaiscribe/quickstart) — set up auth + make your first request.
- [Tutorials → Admin workflows](/docs/scaiscribe/tutorials/admin) — operator-side patterns.
- [Reference → API](/docs/scaiscribe/reference/api) — `/v1/admin/*` shapes.
