Upstash-backed RateLimitPort provider for Beignet applications.
The provider installs ctx.ports.rateLimit using
Upstash Redis and
@upstash/ratelimit.
RateLimitPort interface.bun add @beignet/provider-rate-limit-upstash @upstash/redis @upstash/ratelimit
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 |
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,
});
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;
}
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
});
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",
},
};
}
}
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
}
Ratelimit.fixedWindow()Ratelimit instance for each hit() call to support dynamic limitsWhen @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.
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");
The provider includes comprehensive tests. Run them with:
bun test
MIT