Deploying applications on Unkey is in public beta. To try it, select
Projects in the dashboard sidebar and create a project. Deployed
resources are free until August 1, 2026. We’re eager for feedback, so let us
know what you think on Discord,
X, or email
support@unkey.com.
X-Unkey-Principal header in common frameworks using API key authentication.
import { Hono } from "hono";
type KeyPrincipal = {
version: string;
subject: string;
type: "API_KEY";
identity?: {
externalId: string;
meta: Record<string, unknown>;
};
source: {
key: {
keyId: string;
keySpaceId: string;
name?: string;
expiresAt?: number;
meta: Record<string, unknown>;
roles?: string[];
permissions?: string[];
};
};
};
const app = new Hono();
app.use("*", async (c, next) => {
const header = c.req.header("x-unkey-principal");
if (!header) {
return c.json({ error: "Not authenticated" }, 401);
}
const principal: KeyPrincipal = JSON.parse(header);
c.set("principal", principal);
await next();
});
app.get("/api/resource", (c) => {
const principal = c.get("principal") as KeyPrincipal;
if (!principal.source.key.permissions?.includes("api.read")) {
return c.json({ error: "Insufficient permissions" }, 403);
}
const userId = principal.subject;
const org = principal.identity?.meta?.org;
return c.json({ userId, org });
});
export default app;
import { NextRequest, NextResponse } from "next/server";
type KeyPrincipal = {
version: string;
subject: string;
type: "API_KEY";
identity?: {
externalId: string;
meta: Record<string, unknown>;
};
source: {
key: {
keyId: string;
keySpaceId: string;
name?: string;
expiresAt?: number;
meta: Record<string, unknown>;
roles?: string[];
permissions?: string[];
};
};
};
function getPrincipal(req: NextRequest): KeyPrincipal | null {
const header = req.headers.get("x-unkey-principal");
if (!header) {
return null;
}
return JSON.parse(header) as KeyPrincipal;
}
export async function GET(req: NextRequest) {
const principal = getPrincipal(req);
if (!principal) {
return NextResponse.json(
{ error: "Not authenticated" },
{ status: 401 }
);
}
const userId = principal.subject;
const plan = principal.identity?.meta?.plan ?? "free";
const canWrite = principal.source.key.permissions?.includes("api.write") ?? false;
return NextResponse.json({ userId, plan, canWrite });
}
import express, { Request, Response, NextFunction } from "express";
type KeyPrincipal = {
version: string;
subject: string;
type: "API_KEY";
identity?: {
externalId: string;
meta: Record<string, unknown>;
};
source: {
key: {
keyId: string;
keySpaceId: string;
name?: string;
expiresAt?: number;
meta: Record<string, unknown>;
roles?: string[];
permissions?: string[];
};
};
};
declare global {
namespace Express {
interface Request {
principal?: KeyPrincipal;
}
}
}
const app = express();
app.use((req: Request, res: Response, next: NextFunction) => {
const header = req.header("x-unkey-principal");
if (!header) {
return res.status(401).json({ error: "Not authenticated" });
}
req.principal = JSON.parse(header) as KeyPrincipal;
next();
});
app.get("/api/resource", (req: Request, res: Response) => {
const principal = req.principal!;
if (!principal.source.key.permissions?.includes("api.read")) {
return res.status(403).json({ error: "Insufficient permissions" });
}
const userId = principal.subject;
const plan = principal.identity?.meta?.plan ?? "free";
res.json({ userId, plan });
});
app.listen(8080);
package main
import (
"encoding/json"
"net/http"
"slices"
)
type KeyPrincipal struct {
Version string `json:"version"`
Subject string `json:"subject"`
Type string `json:"type"`
Identity *Identity `json:"identity,omitempty"`
Source struct {
Key KeySource `json:"key"`
} `json:"source"`
}
type Identity struct {
ExternalID string `json:"externalId"`
Meta map[string]any `json:"meta"`
}
type KeySource struct {
KeyID string `json:"keyId"`
KeySpaceID string `json:"keySpaceId"`
Name string `json:"name,omitempty"`
ExpiresAt int64 `json:"expiresAt,omitempty"` // unix milliseconds
Meta map[string]any `json:"meta"`
Roles []string `json:"roles,omitempty"`
Permissions []string `json:"permissions,omitempty"`
}
func parsePrincipal(r *http.Request) (*KeyPrincipal, bool) {
header := r.Header.Get("X-Unkey-Principal")
if header == "" {
return nil, false
}
var p KeyPrincipal
if err := json.Unmarshal([]byte(header), &p); err != nil {
return nil, false
}
return &p, true
}
func handler(w http.ResponseWriter, r *http.Request) {
principal, ok := parsePrincipal(r)
if !ok {
http.Error(w, `{"error":"Not authenticated"}`, http.StatusUnauthorized)
return
}
// Permissions is nil when the key has no permissions attached.
// slices.Contains on a nil slice returns false without panicking.
if !slices.Contains(principal.Source.Key.Permissions, "api.read") {
http.Error(w, `{"error":"Insufficient permissions"}`, http.StatusForbidden)
return
}
userID := principal.Subject
json.NewEncoder(w).Encode(map[string]string{"userId": userID})
}
func main() {
http.HandleFunc("/api/resource", handler)
http.ListenAndServe(":8080", nil)
}