@contract-kit/provider-rate-limit-upstash
v1.0.0
Published
Upstash-based rate limit provider for contract-kit - adds rate limit port using Upstash Redis
Maintainers
Readme
@contract-kit/provider-rate-limit-upstash
Upstash-based implementation of the RateLimitPort for Contract Kit apps.
This provider uses Upstash Redis and the @upstash/ratelimit library to provide distributed rate limiting capabilities to your Contract Kit applications.
Features
- ✅ Implements the standard
RateLimitPortinterface - ✅ Uses Upstash Redis REST API (serverless-friendly, no connection pooling needed)
- ✅ Fixed window rate limiting algorithm
- ✅ Dynamic rate limit configuration per request
- ✅ Configurable key prefix to avoid collisions
- ✅ Type-safe configuration with Zod validation
- ✅ Devtools events for allowed, blocked, and failed hits
- ✅ Works seamlessly with Contract Kit framework and middleware
Installation
npm install @contract-kit/provider-rate-limit-upstash @upstash/redis @upstash/ratelimitOr with other package managers:
bun add @contract-kit/provider-rate-limit-upstash @upstash/redis @upstash/ratelimit
pnpm add @contract-kit/provider-rate-limit-upstash @upstash/redis @upstash/ratelimit
yarn add @contract-kit/provider-rate-limit-upstash @upstash/redis @upstash/ratelimitTypeScript requirements
This package requires TypeScript 5.0 or higher for proper type inference.
Configuration
Set these environment variables:
| Variable | Required | Description | Example |
|----------|----------|-------------|---------|
| UPSTASH_REDIS_REST_URL | Yes | Your Upstash Redis REST URL | https://us1-properly-ancient-12345.upstash.io |
| UPSTASH_REDIS_REST_TOKEN | Yes | Your Upstash Redis REST token | AXXXeyJpZCI6IjEy... |
| UPSTASH_PREFIX | No | Key prefix for rate limit keys (default: ck:ratelimit) | myapp:ratelimit |
Getting Upstash credentials
- Sign up at Upstash
- Create a new Redis database
- Navigate to the database details page
- Copy the REST URL and REST token from the "REST API" section
Usage
With Contract Kit framework
import { createServer } from "@contract-kit/server";
import { upstashRateLimitProvider } from "@contract-kit/provider-rate-limit-upstash";
export const app = await createServer({
ports: basePorts,
providers: [upstashRateLimitProvider],
createContext: ({ ports }) => ({ ports }),
routes: [
// ... your routes
],
mapUnhandledError: () => ({
status: 500,
body: {
code: "INTERNAL_SERVER_ERROR",
message: "Internal server error",
},
}),
});In middleware or policies
Once the provider is registered, you can use the rate limit port in your middleware:
// Example middleware that rate limits by IP address
async function rateLimitMiddleware(ctx: AppCtx) {
const result = await ctx.ports.rateLimit.hit({
key: `ip:${ctx.ip}`,
limit: 100,
windowSec: 60, // 100 requests per 60 seconds
});
if (!result.allowed) {
return {
status: 429,
headers: {
"X-RateLimit-Limit": "100",
"X-RateLimit-Remaining": String(result.remaining ?? 0),
"X-RateLimit-Reset": result.resetAt?.toISOString() ?? "",
"Retry-After": String(result.retryAfterSeconds ?? 0),
},
body: {
code: "TOO_MANY_REQUESTS",
message: "Rate limit exceeded. Please try again later.",
},
};
}
// Request is allowed
return undefined; // continue to next middleware/handler
}Different rate limits for different endpoints
You can apply different rate limits for different operations:
// Strict rate limit for auth endpoints
const loginResult = await ctx.ports.rateLimit.hit({
key: `login:${ctx.ip}`,
limit: 5,
windowSec: 300, // 5 attempts per 5 minutes
});
// More relaxed rate limit for API endpoints
const apiResult = await ctx.ports.rateLimit.hit({
key: `api:user:${userId}`,
limit: 1000,
windowSec: 3600, // 1000 requests per hour
});Using with contract metadata
You can define rate limit metadata on your contracts:
const getTodos = api.get("/todos")
.meta({
rateLimit: { max: 60, windowSec: 60, scope: "user" },
});Then in your middleware, read this metadata and apply rate limits:
async function rateLimitFromMeta(ctx: AppCtx, meta?: any) {
if (!meta?.rateLimit) return;
const { max, windowSec, scope = "global" } = meta.rateLimit;
const result = await ctx.ports.rateLimit.hit({
key: `${scope}:${ctx.userId ?? ctx.ip ?? "global"}`,
limit: max,
windowSec,
});
if (!result.allowed) {
return {
status: 429,
body: {
code: "TOO_MANY_REQUESTS",
message: "Too many requests",
},
};
}
}Rate limit result
The hit method returns a RateLimitResult with:
interface RateLimitResult {
allowed: boolean; // true if the hit is within the limit
remaining: number | null; // requests remaining in the window
resetAt: Date | null; // when the window resets
retryAfterSeconds: number | null; // retry delay when the hit is rejected
}Implementation details
- Algorithm: Uses fixed window rate limiting via
Ratelimit.fixedWindow() - Backend: Upstash Redis REST API (serverless-compatible)
- Per-request configuration: Creates a new
Ratelimitinstance for eachhit()call to support dynamic limits - Key prefix: Configurable prefix to avoid key collisions
Devtools
When @contract-kit/devtools is installed before this provider, rate limit
checks appear under the dashboard's Rate limits watcher.
The provider records rateLimit.hit events with the key, limit, window,
configured prefix, allowed/blocked result, remaining count, reset time,
retry-after value, and duration. Provider failures are recorded as
rateLimit.hit.failed.
Advanced usage
Access the underlying Redis client
The provider extends the standard RateLimitPort with access to the underlying Upstash Redis client:
import type { UpstashRateLimitPort } from "@contract-kit/provider-rate-limit-upstash";
const rateLimit = ctx.ports.rateLimit as UpstashRateLimitPort;
// Access the Redis client for advanced operations
await rateLimit.client.get("some:key");
await rateLimit.client.set("some:key", "value");Testing
The provider includes comprehensive tests. Run them with:
bun testLicense
MIT
