Protect your Cloudflare Workers with Unkey's globally distributed verification.
Install#
npm create cloudflare@latest my-apicd my-apinpm install @unkey/apiBasic Worker#
src/index.ts
import { Unkey } from "@unkey/api";export interface Env { UNKEY_ROOT_KEY: string;}export default { async fetch(request: Request, env: Env): Promise<Response> { // 1. Extract API key const authHeader = request.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) { return Response.json({ error: "Missing API key" }, { status: 401 }); } const unkey = new Unkey({ rootKey: env.UNKEY_ROOT_KEY }); const apiKey = authHeader.slice(7); try { // 2. Verify with Unkey (throws on transport/SDK errors — handled below) const { data } = await unkey.keys.verifyKey({ key: apiKey, }); // 3. Check validity if (!data.valid) { return Response.json( { error: data.code }, { status: data.code === "RATE_LIMITED" ? 429 : 401 }, ); } // 4. Request is authenticated return Response.json({ message: "Access granted", user: data.identity?.externalId, remaining: data.credits, }); } catch (error) { console.error("Unkey error:", error); return Response.json( { error: "Authentication service unavailable" }, { status: 503 }, ); } },};Configure Secrets#
npx wrangler secret put UNKEY_ROOT_KEY# Enter your API ID when promptedWith Hono#
For a cleaner routing experience, use Hono:
npm install hono @unkey/honosrc/index.ts
import { Hono } from "hono";import { unkey } from "@unkey/hono";type Bindings = { UNKEY_ROOT_KEY: string;};const app = new Hono<{ Bindings: Bindings }>();// Public routesapp.get("/health", (c) => c.json({ status: "ok" }));// Protected routes with Unkey middlewareapp.use("/api/*", async (c, next) => { const handler = unkey({ rootKey: c.env.UNKEY_ROOT_KEY, getKey: (c) => c.req.header("Authorization")?.replace("Bearer ", ""), onError: (c, error) => { console.error("Unkey error:", error); return c.json({ error: "Service unavailable" }, 503); }, handleInvalidKey: (c, result) => { return c.json({ error: result.code }, 401); }, }); return handler(c, next);});app.get("/api/data", (c) => { const auth = c.get("unkey"); return c.json({ message: "Access granted", user: auth.identity?.externalId, remaining: auth.credits, });});app.get("/api/users", (c) => { return c.json({ users: [] });});export default app;Reusable Auth Middleware#
Create a clean middleware pattern:
src/middleware/auth.ts
import { Unkey } from "@unkey/api";import { UnkeyError } from "@unkey/api/models/errors";import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components";import { Context, Next } from "hono";declare module "hono" { interface ContextVariableMap { auth: V2KeysVerifyKeyResponseData; }}interface AuthOptions { getKey?: (c: Context) => string | null; permissions?: string;}type Bindings = { UNKEY_ROOT_KEY: string;};export function authMiddleware(options: AuthOptions = {}) { return async (c: Context, next: Next) => { const getKey = options.getKey ?? ((c) => c.req.header("Authorization")?.replace("Bearer ", "") ?? null); const apiKey = getKey(c); if (!apiKey) { return c.json({ error: "Missing API key" }, 401); } try { const unkey = new Unkey({ rootKey: c.env.UNKEY_ROOT_KEY }); const { data } = await unkey.keys.verifyKey({ key: apiKey, permissions: options.permissions, }); if (!data.valid) { if (data.code === "INSUFFICIENT_PERMISSIONS") { return c.json({ error: "Forbidden" }, 403); } return c.json({ error: data.code }, { status: 401 }); } c.set("auth", data); await next(); } catch (err) { return c.json({ error: "Service unavailable" }, 503); } };}Use it:
src/index.ts
import { Hono } from "hono";import { authMiddleware } from "./middleware/auth";const app = new Hono();// Basic authapp.use("/api/*", (c, next) => authMiddleware()(c, next));// Permission-based auth for admin routesapp.use("/admin/*", (c, next) => authMiddleware({ permissions: "admin", })(c, next),);app.get("/api/data", (c) => { const auth = c.get("auth"); return c.json({ user: auth.identity?.externalId });});app.delete("/admin/users/:id", (c) => { // Only accessible with admin permission return c.json({ deleted: c.req.param("id") });});export default app;Rate Limit Headers#
Add standard rate limit headers:
src/middleware/auth.ts
import { Unkey } from "@unkey/api";import { UnkeyError } from "@unkey/api/models/errors";import { V2KeysVerifyKeyResponseData } from "@unkey/api/models/components";export function authMiddleware(options: AuthOptions = {}) { return async (c: Context, next: Next) => { const apiKey = c.req.header("Authorization")?.replace("Bearer ", ""); if (!apiKey) { return c.json({ error: "Missing API key" }, 401); } try { const unkey = new Unkey({ rootKey: c.env.UNKEY_ROOT_KEY }); const { data } = await unkey.keys.verifyKey({ key: apiKey, }); // Set rate limit headers (v2 uses ratelimits array) if (data.ratelimits?.[0]) { const rl = data.ratelimits[0]; c.header("X-RateLimit-Limit", rl.limit.toString()); c.header("X-RateLimit-Remaining", rl.remaining.toString()); c.header("X-RateLimit-Reset", rl.reset.toString()); } if (data.credits !== undefined) { c.header("X-Credits-Remaining", data.credits.toString()); } if (!data.valid) { const status = data.code === "RATE_LIMITED" ? 429 : 401; return c.json({ error: data.code }, status); } c.set("auth", data); await next(); } catch (err) { return c.json({ error: "Service unavailable" }, 503); } };}With Durable Objects#
For stateful applications with Durable Objects:
src/index.ts
import { Unkey } from "@unkey/api";import { DurableObject } from "cloudflare:workers";export interface Env { UNKEY_ROOT_KEY: string;}export class Counter extends DurableObject { async fetch(request: Request) { let count = ((await this.ctx.storage.get("count")) as number) || 0; count++; await this.ctx.storage.put("count", count); return Response.json({ count }); }}export default { async fetch(request: Request, env: Env): Promise<Response> { const unkey = new Unkey({ rootKey: env.UNKEY_ROOT_KEY }); // Verify API key first const apiKey = request.headers.get("Authorization")?.slice(7); if (!apiKey) { return Response.json({ error: "Missing API key" }, { status: 401 }); } try { const { data } = await unkey.keys.verifyKey({ key: apiKey, }); if (!data.valid) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } // Use external ID as Durable Object ID for per-user state const id = env.COUNTER.idFromName( data.identity?.externalId ?? "anonymous", ); const counter = env.COUNTER.get(id); return counter.fetch(request); } catch (error) { return Response.json({ error: "Service unavailable" }, { status: 503 }); } },};wrangler.toml#
wrangler.toml
name = "my-api"main = "src/index.ts"compatibility_date = "2024-01-01"# Store UNKEY_ROOT_KEY as a secret, not in config# npx wrangler secret put UNKEY_ROOT_KEYDeploy#
npx wrangler deployYour API is now protected globally with Unkey! 🌍