Every AI API eventually needs rate limiting, authentication, and usage analytics — but building these from scratch in your Next.js app means re-implementing solved problems. Zuplo is a programmable API gateway that adds rate limiting, API key management, JWT validation, and developer documentation on top of any existing HTTP endpoint. You configure it with code (TypeScript), deploy it globally, and it proxies traffic to your backend.
For AI applications, Zuplo is particularly valuable for: protecting OpenAI-backed endpoints from abuse, issuing API keys to customers, enforcing per-customer rate limits, and generating automatic OpenAPI documentation from your routes.
How Zuplo Works
Zuplo sits in front of your existing API. You define routes in a `routes.oas.json` file (OpenAPI format), attach policies (middleware) to each route, and deploy. Zuplo runs your policies at the edge before proxying to your backend — adding auth validation, rate limiting, and request transformation without changing your backend code.
| Concept | Description |
|---|---|
| Route | An HTTP path + method that Zuplo handles |
| Policy | Middleware attached to a route (auth, rate limit, transform) |
| Handler | Where the request is forwarded — your backend URL |
| Module | A TypeScript file with custom logic (custom policies, request rewriting) |
Setting Up Your First Zuplo Project
Create an account at zuplo.com and start a new project from the dashboard. Zuplo has a browser-based IDE for route configuration — no local setup required, though you can also manage routes as code via the Zuplo CLI.
{ "routes": [ { "path": "/api/v1/chat", "method": "POST", "corsPolicy": "anything-goes", "handler": { "export": "urlForwardHandler", "module": "$import(@zuplo/runtime)", "options": { "baseUrl": "https://yourapp.com/api/chat" } }, "policies": { "inbound": [ "api-key-inbound", "rate-limit-inbound" ] } } ] }
Adding API Key Authentication
Zuplo has a built-in API key management system. Enable the `api-key-inbound` policy and Zuplo handles key validation, storage, and the developer portal automatically.
{ "name": "api-key-inbound", "policyType": "api-key-inbound", "handler": { "export": "ApiKeyInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "allowUnauthenticatedRequests": false } } }
Issue API keys from the Zuplo dashboard or programmatically via the Zuplo Management API:
// Issue an API key for a customer const response = await fetch( `https://api.zuplo.com/v1/accounts/${accountId}/key-buckets/${bucketName}/consumers`, { method: "POST", headers: { Authorization: `Bearer ${process.env.ZUPLO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: `customer-${customerId}`, description: "API access for customer", metadata: { customerId, plan: "pro" }, }), } ); const { apiKey } = await response.json(); // Send apiKey to the customer — store it securely
Configuring Rate Limits
Attach the rate limit policy to any route. Limits can be per API key, per IP, or per a custom identifier extracted from the request.
{ "name": "rate-limit-inbound", "policyType": "rate-limit-inbound", "handler": { "export": "RateLimitInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "rateLimitBy": "consumer", "requestsAllowed": 100, "timeWindowMinutes": 1, "identifier": { "type": "Header", "header": "zuplo-consumer-name" } } } }
For plan-based limits, use a custom policy that reads metadata from the consumer:
// modules/plan-rate-limit.ts import { ZuploContext, ZuploRequest, RateLimitPolicy } from "@zuplo/runtime"; export default async function planRateLimit( request: ZuploRequest, context: ZuploContext, policyName: string ) { const consumer = context.consumer; const plan = consumer?.metadata?.plan ?? "free"; const limits: Record<string, number> = { free: 10, starter: 100, pro: 1000, enterprise: 10000, }; const requestsAllowed = limits[plan] ?? 10; return new RateLimitPolicy(policyName).apply(request, context, { rateLimitBy: "consumer", requestsAllowed, timeWindowMinutes: 1, }); }
JWT Validation
If your users authenticate with JWTs (from Clerk, Auth0, Supabase, or your own issuer), Zuplo validates them before the request reaches your backend:
{ "name": "jwt-auth-inbound", "policyType": "open-id-jwt-auth-inbound", "handler": { "export": "OpenIdJwtInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "issuerUrl": "https://your-auth-domain.clerk.accounts.dev", "audience": "your-api-identifier", "allowUnauthenticatedRequests": false } } }
Accessing Consumer Identity in Your Backend
Zuplo forwards consumer identity information as headers to your backend:
// Your backend endpoint (Next.js route) export async function POST(req: Request) { // Zuplo injects these headers after validating the API key const consumerId = req.headers.get("zuplo-consumer-name"); const consumerMetadata = req.headers.get("zuplo-consumer-metadata"); const plan = JSON.parse(consumerMetadata ?? "{}").plan; // Use these to apply backend-level business logic const model = plan === "enterprise" ? "gpt-4o" : "gpt-4o-mini"; // ... generate response }
Zuplo's Developer Portal automatically generates interactive API documentation from your routes.oas.json. Share it with customers so they can test your API directly in the browser — no Swagger UI setup needed.Deployment
Zuplo deploys globally to 200+ locations automatically. No servers to manage — push your route configuration and it's live in seconds. Your backend (Next.js on Vercel, FastAPI on Railway, etc.) doesn't change; Zuplo sits in front of it.