@beignet/provider-rate-limit-upstash
v0.0.3
Published
Upstash-based rate limit provider for Beignet - adds rate limit port using Upstash Redis
Maintainers
Readme
@beignet/provider-rate-limit-upstash
Upstash-backed RateLimitPort provider for Beignet applications.
The provider installs ctx.ports.rateLimit using
Upstash Redis and
@upstash/ratelimit.
Features
- Implements the standard
RateLimitPortinterface. - Uses the Upstash Redis REST API, so it is serverless-friendly.
- Supports dynamic limits per request with a configurable key prefix.
- Emits devtools events for allowed, blocked, and failed hits.
Install
bun add @beignet/provider-rate-limit-upstash @upstash/redis @upstash/ratelimitConfiguration
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
Setup
import { createNextServer } from "@beignet/next";
import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";
import { createRateLimitHooks } from "@beignet/core/server";
import type { AppContext } from "@/app-context";
import { appPorts } from "@/infra/app-ports";
import { routes } from "@/server/routes";
export const server = await createNextServer({
ports: appPorts,
providers: [upstashRateLimitProvider],
hooks: [createRateLimitHooks<AppContext>()],
createContext: ({ ports }) => ({ ports }),
routes,
});Direct use
Once the provider is registered, you can use the rate limit port in hooks, policies, or use cases:
// Example app-specific policy that rate limits by IP address
async function checkIpRateLimit(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;
}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" },
});The built-in createRateLimitHooks(...) helper reads this metadata and applies
the limit through ctx.ports.rateLimit. If your app needs custom behavior, keep
the same metadata shape and call the port directly:
type RateLimitMetadata = {
rateLimit?: {
max: number;
windowSec: number;
scope?: "global" | "ip" | "user";
};
};
async function rateLimitFromMeta(ctx: AppCtx, meta?: RateLimitMetadata) {
if (!meta?.rateLimit) return;
const { max, windowSec, scope = "global" } = meta.rateLimit;
const actorId =
ctx.actor?.type === "user" && ctx.actor.id ? ctx.actor.id : undefined;
const result = await ctx.ports.rateLimit.hit({
key:
scope === "user"
? `user:${actorId ?? "anonymous"}`
: `${scope}:${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 @beignet/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 "@beignet/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
