# @suaveplan/api-rate-limit — per-route / per-principal / per-tenant / per-IP token-bucket middleware for the @suaveplan/api plugin system. Implements the RequestRateLimiter contract consumed by @suaveplan/api-host's request pipeline and provides a Hono-shaped Middleware that returns 429 + Retry-After on denial. Backed by @suaveplan/rate-limiter's TokenBucket. In-memory store ships by default; the RateLimitStoreLike interface keeps Redis/distributed adapters pluggable. Per-route / per-principal / per-tenant / per-IP token-bucket rate-limit middleware for `@suaveplan/api`. Implements the `RequestRateLimiter` contract consumed by `@suaveplan/api-host`'s per-request pipeline (design doc §6 step 4a) and ships a richer `check`-based middleware surface for adapter integrations that want the 429 response shape pre-computed. Status: Stage-8 keystone (`0.1.0`). In-memory store ships by default; the `RateLimitStoreLike` interface keeps a future Redis adapter pluggable without changing call sites. ## Documentation tree - [error-codes](./src/error-codes/error-codes.md) — Defines the stable `RATELIMIT.*` error-code namespace for the `@suaveplan/api-rate-limit` package and the `RateLimitErr… - [error-codes](./src/error-codes/error-codes.md) — Defines the stable `RATELIMIT.*` error-code namespace for the `@suaveplan/api-rate-limit` package and the `RateLimitErr… - [instruments](./src/instruments/instruments.md) — Owns the rate-limit OTel instrument registry. Today the package emits exactly one counter — `api.rate_limit.exceeded_to… - [instruments](./src/instruments/instruments.md) — Owns the rate-limit OTel instrument registry. Today the package emits exactly one counter — `api.rate_limit.exceeded_to… - [middleware](./src/middleware/middleware.md) — The package's centerpiece. `createRateLimitMiddleware` builds a per-request gate around the token-bucket store; the gat… - [middleware](./src/middleware/middleware.md) — The package's centerpiece. `createRateLimitMiddleware` builds a per-request gate around the token-bucket store; the gat… - [selector](./src/selector/selector.md) — Resolves the rate-limit bucket key for a per-request frame, given the route-declared (or host-defaulted) `RateLimitKeyS… - [selector](./src/selector/selector.md) — Resolves the rate-limit bucket key for a per-request frame, given the route-declared (or host-defaulted) `RateLimitKeyS… - [memory-store](./src/store/memory-store/memory-store.md) — Provides the default in-memory implementation of `RateLimitStoreLike`. Backed by `TokenBucket` from `@suaveplan/rate-li… - [memory-store](./src/store/memory-store/memory-store.md) — Provides the default in-memory implementation of `RateLimitStoreLike`. Backed by `TokenBucket` from `@suaveplan/rate-li… - [store-like](./src/store/store-like/store-like.md) — Declares the `RateLimitStoreLike` interface — the storage abstraction behind the per-route token-bucket middleware. Kee… - [store-like](./src/store/store-like/store-like.md) — Declares the `RateLimitStoreLike` interface — the storage abstraction behind the per-route token-bucket middleware. Kee… ## Why this package exists The host pipeline's `RequestRateLimiter.consume(key, weight)` slot is synchronous and intentionally minimal. Plugins want richer policy — `{ rpm, burst, key }` per route, plus a way for the host to declare global defaults in `host.json`. This package owns both ends: the token-bucket store, the key-selector resolution, the OTel counter emission, and the projection back onto the host's narrow `consume` shape. ## Modules | Subpath | Surface | |---|---| | `@suaveplan/api-rate-limit` | Aggregate barrel — every public symbol. | | `@suaveplan/api-rate-limit/middleware` | `createRateLimitMiddleware`, `RateLimitRouteOption`, `RateLimitDecision`, `HostRateLimitDefaults`, `RateLimitRouteOptionSchema`. | | `@suaveplan/api-rate-limit/selector` | `RateLimitKeySelector`, `resolveRateLimitKey`, `RateLimitContext`. | | `@suaveplan/api-rate-limit/store` | `RateLimitStoreLike`, `MemoryRateLimitStore`, `createMemoryRateLimitStore`, `RateLimitBudget`, `RateLimitTakeResult`. | | `@suaveplan/api-rate-limit/instruments` | `createRateLimitInstruments`, `RATE_LIMIT_COUNTER_NAMES`. | | `@suaveplan/api-rate-limit/error-codes` | `RATELIMIT_CODES`, `RateLimitError`, `RateLimitCode`. | ## Quick start ```ts import { createRateLimitMiddleware } from "@suaveplan/api-rate-limit"; const mw = createRateLimitMiddleware({ hostDefaults: { perTenant: { rpm: 600, burst: 30 }, perIp: { rpm: 60, burst: 10 }, }, }); const decision = mw.check({ plugin: "billing", route: "POST /invoices", option: { rpm: 60, burst: 5, key: "principal" }, ctx: requestFrame, }); if (!decision.allowed) { return new Response(null, { status: 429, headers: decision.headers }); } ``` ## Wiring into `@suaveplan/api-host` ```ts import { createRateLimitMiddleware } from "@suaveplan/api-rate-limit"; const mw = createRateLimitMiddleware({ /* opts */ }); const host = createHost({ rateLimit: mw.asRequestRateLimiter({ plugin: "billing", route: "POST /invoices", option: { rpm: 60, burst: 5 }, keyKind: "principal", }), // ... }); ``` ## Acceptance Pre-archive gate: ``` bunx --bun turbo lint typecheck test:coverage --filter=@suaveplan/api-rate-limit ``` 100% statements/branches/functions/lines (Istanbul).