Unkey
API key management, rate limiting, usage tracking, key verification, temporary keys, ratelimit API, and analytics with Unkey
Unkey is purpose-built infrastructure for API key management. Instead of rolling your own key table, hashing logic, and rate limiting middleware, Unkey provides a managed service where keys are created, verified, and revoked through a simple API. Every verification returns metadata, remaining quota, and rate limit status in a single round-trip. This lets you enforce per-key rate limits, track usage, issue temporary keys, and build analytics without maintaining any of that plumbing yourself. ## Key Points - Always use a `prefix` when creating keys (e.g., `sk_`, `tmp_`) so users and logs can identify key types at a glance. - Set `remaining` on keys to enforce hard usage caps independent of rate limits. Rate limits control speed; remaining controls total volume. - Use `ownerId` on every key so you can list and revoke all keys for a user when needed. - Store business logic in `meta` (plan tier, permissions) and read it on verification rather than maintaining a separate lookup table. - Return standard `X-RateLimit-*` headers so API consumers can implement proper backoff. - Use separate Ratelimit namespaces for different resource types (e.g., `api.read` vs. `api.write`) to apply distinct limits. - For temporary access (trials, previews), use `expires` and `remaining` together so the key is bounded in both time and usage. - **Storing the key plaintext in your own database.** Unkey handles hashing. You only need the `keyId` for management operations; never persist the raw key after returning it to the user. - **Verifying keys without checking `result.valid`.** The API call can succeed (no `error`) but return `valid: false` for expired or revoked keys. Always check the boolean. - **Using a single namespace for all rate limits.** This conflates different resources and makes it impossible to tune limits independently. - **Creating keys without `ownerId`.** You lose the ability to query or revoke keys by user, making key management painful at scale. - **Ignoring the `remaining` field.** If you set a usage quota, you must communicate the remaining count to the user or they have no way to plan their consumption. ## Quick Example ```typescript npm install @unkey/api @unkey/ratelimit ``` ```typescript // .env.local UNKEY_ROOT_KEY=unkey_yourRootKey UNKEY_API_ID=api_yourApiId ```
skilldb get security-ratelimit-skills/UnkeyFull skill: 369 linesUnkey: API Key Infrastructure
Core Philosophy
Unkey is purpose-built infrastructure for API key management. Instead of rolling your own key table, hashing logic, and rate limiting middleware, Unkey provides a managed service where keys are created, verified, and revoked through a simple API. Every verification returns metadata, remaining quota, and rate limit status in a single round-trip. This lets you enforce per-key rate limits, track usage, issue temporary keys, and build analytics without maintaining any of that plumbing yourself.
Setup
Installation
npm install @unkey/api @unkey/ratelimit
Environment Configuration
// .env.local
UNKEY_ROOT_KEY=unkey_yourRootKey
UNKEY_API_ID=api_yourApiId
Client Initialization
// lib/unkey.ts
import { Unkey } from "@unkey/api";
export const unkey = new Unkey({
rootKey: process.env.UNKEY_ROOT_KEY!,
});
Key Techniques
Creating API Keys
// app/api/keys/create/route.ts
import { unkey } from "@/lib/unkey";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const { userId, plan } = await req.json();
const result = await unkey.keys.create({
apiId: process.env.UNKEY_API_ID!,
prefix: "sk",
ownerId: userId,
meta: {
plan,
createdBy: "api",
},
ratelimit: {
type: "fast",
limit: plan === "pro" ? 1000 : 100,
refillRate: plan === "pro" ? 100 : 10,
refillInterval: 60000, // ms
},
remaining: plan === "pro" ? 50000 : 5000,
});
if (result.error) {
return NextResponse.json({ error: result.error.message }, { status: 500 });
}
return NextResponse.json({
key: result.result.key,
keyId: result.result.keyId,
});
}
Verifying Keys
// lib/verify-key.ts
import { verifyKey } from "@unkey/api";
export async function verifyApiKey(key: string) {
const result = await verifyKey({
key,
apiId: process.env.UNKEY_API_ID!,
});
if (result.error) {
return { valid: false, error: result.error.message };
}
return {
valid: result.result.valid,
ownerId: result.result.ownerId,
meta: result.result.meta,
remaining: result.result.remaining,
ratelimit: result.result.ratelimit,
};
}
API Route with Key Verification
// app/api/protected/route.ts
import { verifyApiKey } from "@/lib/verify-key";
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
const authHeader = req.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) {
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
}
const key = authHeader.slice(7);
const verification = await verifyApiKey(key);
if (!verification.valid) {
return NextResponse.json(
{ error: "Invalid or expired API key" },
{ status: 403 }
);
}
if (verification.ratelimit && !verification.ratelimit.remaining) {
return NextResponse.json(
{ error: "Rate limit exceeded", resetAt: verification.ratelimit.reset },
{ status: 429 }
);
}
return NextResponse.json({
data: "protected resource",
remainingCredits: verification.remaining,
});
}
Temporary Keys
// app/api/keys/temporary/route.ts
import { unkey } from "@/lib/unkey";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const { userId, durationHours } = await req.json();
const expires = Date.now() + durationHours * 60 * 60 * 1000;
const result = await unkey.keys.create({
apiId: process.env.UNKEY_API_ID!,
prefix: "tmp",
ownerId: userId,
expires,
meta: {
type: "temporary",
purpose: "trial",
},
remaining: 500,
});
if (result.error) {
return NextResponse.json({ error: result.error.message }, { status: 500 });
}
return NextResponse.json({
key: result.result.key,
expiresAt: new Date(expires).toISOString(),
});
}
Standalone Rate Limiting (Without Keys)
// lib/ratelimit.ts
import { Ratelimit } from "@unkey/ratelimit";
const limiter = new Ratelimit({
rootKey: process.env.UNKEY_ROOT_KEY!,
namespace: "api.public",
limit: 30,
duration: "60s",
});
export async function checkRateLimit(identifier: string) {
const result = await limiter.limit(identifier);
return {
success: result.success,
remaining: result.remaining,
reset: result.reset,
};
}
Rate Limiting in Middleware
// middleware.ts
import { Ratelimit } from "@unkey/ratelimit";
import { NextRequest, NextResponse } from "next/server";
const limiter = new Ratelimit({
rootKey: process.env.UNKEY_ROOT_KEY!,
namespace: "middleware.global",
limit: 60,
duration: "60s",
});
export async function middleware(req: NextRequest) {
const ip = req.headers.get("x-forwarded-for") ?? "127.0.0.1";
const result = await limiter.limit(ip);
if (!result.success) {
return NextResponse.json(
{ error: "Too many requests" },
{
status: 429,
headers: {
"X-RateLimit-Limit": result.limit.toString(),
"X-RateLimit-Remaining": result.remaining.toString(),
"X-RateLimit-Reset": result.reset.toString(),
},
}
);
}
const response = NextResponse.next();
response.headers.set("X-RateLimit-Limit", result.limit.toString());
response.headers.set("X-RateLimit-Remaining", result.remaining.toString());
response.headers.set("X-RateLimit-Reset", result.reset.toString());
return response;
}
export const config = {
matcher: ["/api/:path*"],
};
Key Update and Revocation
// lib/key-management.ts
import { unkey } from "@/lib/unkey";
export async function updateKeyMeta(keyId: string, meta: Record<string, unknown>) {
const result = await unkey.keys.update({
keyId,
meta,
});
return result;
}
export async function revokeKey(keyId: string) {
const result = await unkey.keys.delete({
keyId,
});
return result;
}
export async function updateKeyRateLimit(keyId: string, limit: number, refillRate: number) {
const result = await unkey.keys.update({
keyId,
ratelimit: {
type: "fast",
limit,
refillRate,
refillInterval: 60000,
},
});
return result;
}
Usage Tracking and Remaining Quota
// app/api/usage/route.ts
import { verifyApiKey } from "@/lib/verify-key";
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
const key = req.headers.get("authorization")?.slice(7);
if (!key) {
return NextResponse.json({ error: "Missing key" }, { status: 401 });
}
const verification = await verifyApiKey(key);
if (!verification.valid) {
return NextResponse.json({ error: "Invalid key" }, { status: 403 });
}
return NextResponse.json({
remaining: verification.remaining,
ratelimit: verification.ratelimit
? {
limit: verification.ratelimit.limit,
remaining: verification.ratelimit.remaining,
reset: verification.ratelimit.reset,
}
: null,
plan: (verification.meta as Record<string, string>)?.plan ?? "free",
});
}
Multi-Namespace Rate Limiting
// lib/multi-ratelimit.ts
import { Ratelimit } from "@unkey/ratelimit";
const globalLimiter = new Ratelimit({
rootKey: process.env.UNKEY_ROOT_KEY!,
namespace: "global",
limit: 100,
duration: "60s",
});
const aiLimiter = new Ratelimit({
rootKey: process.env.UNKEY_ROOT_KEY!,
namespace: "ai.generation",
limit: 5,
duration: "60s",
});
export async function checkLimits(identifier: string, isAiRoute: boolean) {
const globalResult = await globalLimiter.limit(identifier);
if (!globalResult.success) {
return { allowed: false, reason: "global_limit" };
}
if (isAiRoute) {
const aiResult = await aiLimiter.limit(identifier);
if (!aiResult.success) {
return { allowed: false, reason: "ai_limit" };
}
}
return { allowed: true };
}
Best Practices
- Always use a
prefixwhen creating keys (e.g.,sk_,tmp_) so users and logs can identify key types at a glance. - Set
remainingon keys to enforce hard usage caps independent of rate limits. Rate limits control speed; remaining controls total volume. - Use
ownerIdon every key so you can list and revoke all keys for a user when needed. - Store business logic in
meta(plan tier, permissions) and read it on verification rather than maintaining a separate lookup table. - Return standard
X-RateLimit-*headers so API consumers can implement proper backoff. - Use separate Ratelimit namespaces for different resource types (e.g.,
api.readvs.api.write) to apply distinct limits. - For temporary access (trials, previews), use
expiresandremainingtogether so the key is bounded in both time and usage.
Anti-Patterns
- Storing the key plaintext in your own database. Unkey handles hashing. You only need the
keyIdfor management operations; never persist the raw key after returning it to the user. - Verifying keys without checking
result.valid. The API call can succeed (noerror) but returnvalid: falsefor expired or revoked keys. Always check the boolean. - Using a single namespace for all rate limits. This conflates different resources and makes it impossible to tune limits independently.
- Creating keys without
ownerId. You lose the ability to query or revoke keys by user, making key management painful at scale. - Ignoring the
remainingfield. If you set a usage quota, you must communicate the remaining count to the user or they have no way to plan their consumption. - Hardcoding the root key in source. The root key has full account access. Keep it in environment variables and rotate it periodically.
Install this skill directly: skilldb add security-ratelimit-skills
Related Skills
Arcjet
Rate limiting, bot detection, email validation, and shield attack protection using Arcjet with Next.js middleware and stacking rules
Cloudflare Turnstile
Privacy-preserving CAPTCHA alternative using Cloudflare Turnstile for bot protection with server-side verification in Next.js and Express
Security Headers
Security headers with Helmet.js, Content Security Policy, CORS configuration, CSRF protection, rate limiting patterns, and Next.js security headers
OWASP ZAP
Automated web application security testing, API scanning, and CI/CD DAST integration using OWASP ZAP
Snyk
Dependency vulnerability scanning, license compliance, and continuous security monitoring using Snyk CLI and CI/CD integrations
Svix
Webhook delivery infrastructure including sending webhooks, retry logic, signature verification, event types, consumer portal, and message logging with Svix