Quotas & costs — usage and spending ceilings
The quota engine covers three kinds: the token quota and the storage quota (Phase 207) plus the cost quota (Phase 216, hold-and-settle over real-money math). Quotas are the operator's tool for usage ceilings in multi-user installs; they bind every user *…
The quota engine covers three kinds: the token quota and the storage quota (Phase 207) plus the cost quota (Phase 216, hold-and-settle over real-money math). Quotas are the operator's tool for usage ceilings in multi-user installs; they bind every user with a configured quota — admins included — and an unset quota means unlimited (fail-open: zero behavior change for existing instals).
Community feature (an operator tool). Sources:docs/QUOTAS.md(Phases 207 + 216),docs/MODEL_COSTS.md,docs/API.md(the Costs, quotas, and per-user model policy section). Provenance rows:FACT-CENSUS.mdF-QUOTA-1.
Core invariants
- No second meter. The token/cost consumption truth stays inside the EXISTING
WorkspaceTokenUsageledger (packages/server/src/services/quotaService.ts).QuotaBalance/QuotaHold/QuotaResetare derived state and anchors, never a counter table. - Reset = a ledger anchor. A reset writes the
QuotaReset(kind, at)anchor row bounding the rolling window — the usage/cost history is never deleted or zeroed. - Service-level, pre-turn gates — NOT auth middleware (the check needs domain data, not the request shape). The gates fire before the first LLM call in the orchestrator and before the ingest in the upload paths.
- Fail-open for unset quotas. The resolution chain ends at "unlimited" when nothing is configured (D-08).
- Money math is Decimal→cents. ALL cost arithmetic converts through
Prisma.Decimaland lands as INTEGER cents — never float (costQuotaService.ts, the COST-01 doctrine).
The resolution chain (D-08)
| Tier | Source | Notes |
|---|---|---|
| 1 | The per-user override | User.tokenQuotaLimit / User.storageQuotaGb / per-user cost rows — via PUT /api/quota/:userId (admin) |
| 2 | The install preset | SystemConfig keys (below) — DB > ENV > default |
| 3 | Unlimited | The "0" / "" / unset sentinel = NOT configured → fail-open |
Configuration keys (SystemConfig, admin-editable via Settings)
| Key | Default | Meaning |
|---|---|---|
QUOTA_TOKEN_DEFAULT | the unset sentinel | The install-level default token ceiling for every user without an override |
QUOTA_STORAGE_GB_DEFAULT | the unset sentinel | The install-level default storage ceiling (GB) |
QUOTA_COST_EUR_DEFAULT | the unset sentinel ("0"/"" = unlimited) | The install-level default COST ceiling — an EUR decimal string |
QUOTA_COST_FALLBACK_INPUT_CENTS_PER_1M | a non-zero built-in floor | The SC-4 fallback rate for UNPRICED models — input, integer cents per 1M tokens |
QUOTA_COST_FALLBACK_OUTPUT_CENTS_PER_1M | a non-zero built-in floor | The SC-4 fallback rate for UNPRICED models — output |
The SC-4 doctrine: a model without pricing NEVER bypasses the cost quota — the fallback rates apply with a warn-once log (warnFallbackOnce), so an unpriced model can never appear free.
Token quotas (quotaService.ts)
resolveTokenQuota(userId)→{ limit, source }—limit: nullmeans unlimited;sourcenames the chain tier that decided.- The rolling window starts at the latest
QuotaResetanchor for(userId, kind: "token")(or the epoch when never reset); the windowed usage sums from theWorkspaceTokenUsageledger. - Exceeding the quota raises
QuotaError(status+payload) — the payload'squotafield is the contract the frontend, the widget, and the connector handlers key on. There is no middleware to bypass — the gate runs inside the service path.
Cost quotas (costQuotaService.ts, Phase 216) — hold-and-settle
A two-phase reservation:
- Reserve (before the LLM call) —
reserveCostQuota(principal, estimateCents)runs ONE interactive transaction: a conditionalupdateManywhose predicate (spentCents + estimate <= limitCents) Postgres evaluates row-locked, so N concurrent gates SERIALIZE instead of all passing a stale check (SC-2) + theQuotaHoldinsert in the same transaction. - Settle (at the usage seam) —
settleCostHoldfinalizes the hold and refundsestimate − actual. The in-flight overshoot (actual > estimate) is the accepted window — no mid-stream kill. - At rest the invariant holds:
settledSpent + Σ(open holds) ≤ limit. - The pricing source per row: the FROZEN Phase 203 snapshot where one exists; otherwise the SC-4 fallback rates (warn-once); rows in a currency outside the ledger's dominant one are still priced (never silently free, never excluded).
- Reset —
resetCostQuotawrites theQuota(kind "cost")anchor and REBUILDS the balance from the ledger sum + the open holds (D-02).
Storage quotas
checkStorageQuota / resolveStorageQuota enforce the per-user storageQuotaGb override, the QUOTA_STORAGE_GB_DEFAULT preset, or unlimited. The gates fire in the upload paths before dispatching to the collector.
The widget quota principal
Anonymous widget visitors are not users — resolveWidgetQuotaPrincipal binds widget traffic to the widget's organization principal: widget consumption counts against the widget org's quota chain rather than a per-visitor identity.
Where the gates fire (the consumption sites)
| Surface | Gate | File |
|---|---|---|
| Chat (JWT, widget proxy, connectors — one orchestrator) | the token + cost gates pre-turn / pre-LLM | packages/server/src/agent/orchestrator.ts |
| Document upload | the storage gate pre-ingest | packages/server/src/routes/uploads.ts, routes/documents.ts |
| The reset sweep | the hourly token-quota reset (the pg-boss cron: initQuotaResetScheduler) | services/quotaResetJob.ts |
| Self-service reads | GET /api/quota/me, rollups, manual reset | routes/quota.ts |
The operator workflow
- Set the install defaults — the Settings UI → the
QUOTA_*keys (orPUT /api/system/settings). - The per-user overrides —
PUT /api/quota/:userId(admin) or the quota UI. - The per-user pricing — the COSTS family (Phase 209:
/api/users/:userId/pricing, the own-provider keys under/api/me/provider-keys) feeds the same cost engine. - A manual reset any time:
POST /api/quota/:userId/reset; the hourly sweep handles the steady state.
Model pricing (MODEL_COSTS)
Setup: Settings → Providers → pick the provider → the model's pricing action → the per-1M-token input/output cost (e.g. 1.50 and 6.00) + the currency → Save. The formula: cost = tokens × rate, computed server-side on every token-usage write, snapshot-frozen at run time (later rate edits never rewrite history). Currencies: USD, EUR, GBP, JPY, CNY, INR — per-currency totals, no conversion in v1. N/A = pricing not configured (unset cloud models; the UI shows "N/A" — the admin should configure the rate); $0.00 = explicit free (the local/Ollama models are backfilled to 0 at migration; overridable). v1 limitations: no cross-currency conversion; the main-iteration cost only (the tool/MCP/synthesis sub-pipeline costs excluded); the cost lands at the done event (not mid-stream).
The endpoint reference
The complete endpoint table lives in API reference → Costs, quotas, and per-user model policy.
See also
- Deployment/Scaling — the quota behavior under multi-instance deployments
This guide covers the operator surface for the quotas and costs; the engineering deep dive lives in the twin post:
Provenance
| Section | Source |
|---|---|
| Invariants / the resolution chain / the keys | docs/QUOTAS.md (the core invariants, D-08, the keys — Phase 207/203) — F-QUOTA-1 |
| Cost quotas / hold-and-settle | docs/QUOTAS.md (§ Cost quotas — Phase 216) + costQuotaService.ts |
| Model pricing | docs/MODEL_COSTS.md (Phases 203/209/216) |
| The gate sites | docs/QUOTAS.md (§ Where the gates fire) |
---
Operator reference — Simmetric Chat v0.28.1 (docs refreshed 2026-10-02, census SHA `aca5b8ff`).