Skip to main content

Overview

This guide covers everything you need to protect your Hono application with Unkey using the official @unkey/hono middleware. Hono works everywhere: Node.js, Bun, Cloudflare Workers, and more. What you’ll learn:
  • Quick setup with @unkey/hono
  • Protecting specific routes
  • Custom error handling
  • Permission-based access
  • Deployment patterns

Prerequisites

Quick Start

1

Create a Hono app

npm create hono@latest unkey-hono
cd unkey-hono
Choose your preferred runtime when prompted.
2

Install the Unkey middleware

npm install @unkey/hono
3

Add your root key

Create a .env file:
.env
UNKEY_ROOT_KEY="unkey_..."
The Hono middleware verifies keys directly against your root key.
4

Add the middleware

Update src/index.ts:
src/index.ts
import { Hono } from "hono";
import { unkey, UnkeyContext } from "@unkey/hono";

// Type the context so you get autocomplete for c.get("unkey")
const app = new Hono<{ Variables: { unkey: UnkeyContext } }>();

// Protect all routes with API key authentication
app.use("*", unkey({
  rootKey: process.env.UNKEY_ROOT_KEY!,
}));

// This route now requires a valid API key
app.get("/", (c) => {
  // Access verification result from context
  const keyInfo = c.get("unkey");
  
  return c.json({
    message: "Hello from protected route!",
    keyId: keyInfo.keyId,
    valid: keyInfo.valid,
  });
});

// Another protected route
app.get("/secret", (c) => {
  const keyInfo = c.get("unkey");
  
  return c.json({
    secret: "data",
    identity: keyInfo.identity,
    meta: keyInfo.meta,
  });
});

export default app;
5

Run your app

npm run dev
6

Test it

Create a test key in your Unkey dashboard, then:
Test with valid key
curl http://localhost:3000 \
  -H "Authorization: Bearer YOUR_API_KEY"
You should see:
{
  "message": "Hello from protected route!",
  "keyId": "key_...",
  "valid": true
}
Without a key, you’ll get a 401:
Test without key
curl http://localhost:3000

Protecting Specific Routes

Instead of protecting all routes, apply middleware to specific paths:
src/index.ts
import { Hono } from "hono";
import { unkey, UnkeyContext } from "@unkey/hono";

const app = new Hono<{ Variables: { unkey: UnkeyContext } }>();

// Public route — no middleware
app.get("/", (c) => {
  return c.json({ message: "Welcome! This route is public." });
});

// Protected routes — apply middleware to /api/* paths only
app.use("/api/*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }));

app.get("/api/secret", (c) => {
  const keyInfo = c.get("unkey");
  return c.json({ secret: "data", keyId: keyInfo.keyId });
});

app.get("/api/user", (c) => {
  const keyInfo = c.get("unkey");
  return c.json({ identity: keyInfo.identity });
});

export default app;

Middleware Options

Customize the middleware behavior:
app.use("*", unkey({
  rootKey: process.env.UNKEY_ROOT_KEY!,  // Required: Your root key
  
  // Optional: Custom error handling
  onError: (c, error) => {
    console.error("Unkey error:", error);
    return c.json({ error: "Auth service unavailable" }, 503);
  },
  
  // Optional: Custom unauthorized response
  handleInvalidKey: (c, result) => {
    return c.json({ 
      error: "Invalid API key",
      code: result.code 
    }, 401);
  },
}));

Custom Key Extraction

By default, the middleware looks for Authorization: Bearer <token>. You can customize this:
app.use("/api/*", unkey({
  rootKey: process.env.UNKEY_ROOT_KEY!,
  getKey: (c) => c.req.header("x-api-key"),  // Custom header
}));

Permission-Based Routes

Require specific permissions for certain routes:
src/index.ts
import { Hono } from "hono";
import { unkey, UnkeyContext } from "@unkey/hono";

const app = new Hono<{ Variables: { unkey: UnkeyContext } }>();

// Basic auth for all API routes
app.use("/api/*", unkey({ rootKey: process.env.UNKEY_ROOT_KEY! }));

// Public data endpoint
app.get("/api/data", (c) => {
  return c.json({ data: "public" });
});

// Admin endpoint with permission check
app.delete("/api/users/:id", async (c) => {
  const keyInfo = c.get("unkey");
  
  // Check if user has admin permission
  if (!keyInfo.permissions?.includes("admin.delete")) {
    return c.json({ error: "Admin access required" }, 403);
  }
  
  const userId = c.req.param("id");
  // Delete user logic here
  return c.json({ deleted: userId });
});

export default app;
Or verify permissions during the initial key check:
import { Unkey } from "@unkey/api";

const requireAdmin = async (c: Context, next: Next) => {
  const authHeader = c.req.header("Authorization");
  const apiKey = authHeader?.replace("Bearer ", "");
  
  if (!apiKey) {
    return c.json({ error: "Missing API key" }, 401);
  }
  
  const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! });
  const { data } = await unkey.keys.verifyKey({
    key: apiKey,
    permissions: ["admin"],
  });
  
  if (!data.valid) {
    if (data.code === "INSUFFICIENT_PERMISSIONS") {
      return c.json({ error: "Admin access required" }, 403);
    }
    return c.json({ error: "Unauthorized" }, 401);
  }
  
  c.set("unkey", data);
  await next();
};

app.delete("/api/admin/users/:id", requireAdmin, (c) => {
  return c.json({ deleted: c.req.param("id") });
});

Context Data

After verification, c.get("unkey") contains:
FieldTypeDescription
validbooleanWhether the key passed all checks
codestringStatus code (VALID, NOT_FOUND, RATE_LIMITED, etc.)
keyIdstringThe key’s unique identifier
namestring?Human-readable name of the key
metaobject?Custom metadata associated with the key
expiresnumber?Unix timestamp (in milliseconds) when the key will expire
creditsnumber?Remaining uses (if usage limits set)
enabledbooleanWhether the key is enabled
rolesstring[]?Named role(s) assigned to the key
permissionsstring[]?List of individual permissions
identityobject?Identity info if externalId was set
ratelimitsobject[]?Rate limit states (if configured)

Deployment

Node.js

npm run build
node dist/index.js

Bun

bun run src/index.ts

Cloudflare Workers

For Cloudflare Workers, use wrangler secrets:
npx wrangler secret put UNKEY_ROOT_KEY
Update your worker to read from env:
src/index.ts
import { Hono } from "hono";
import { unkey } from "@unkey/hono";

type Bindings = {
  UNKEY_ROOT_KEY: string;
};

const app = new Hono<{ Bindings: Bindings }>();

app.use("/api/*", async (c, next) => {
  const handler = unkey({
    rootKey: c.env.UNKEY_ROOT_KEY,  // Read from environment
  });
  return handler(c, next);
});

export default app;
Deploy:
npx wrangler deploy

Next Steps

Add rate limiting

Limit requests per key

Set usage limits

Limit requests with credit-based billing

Add permissions

Fine-grained access control

Hono SDK Reference

Full middleware documentation

Troubleshooting

  • Ensure the key hasn’t expired or been revoked
  • Verify the header format: Authorization: Bearer YOUR_KEY
  • For Node.js: Install dotenv and add import 'dotenv/config' at the top
  • For Bun: .env is loaded automatically
  • For Cloudflare Workers: Use wrangler secret or wrangler.toml

Build docs developers (and LLMs) love