- Custom prefix —
sk_live_,pk_test_, etc. - Custom length — Configure key length (default: 32)
- Custom alphabet — URL-safe by default, customizable
- Salt for hashing — Additional security layer
- SHA-256/SHA-512 — Choice of hashing algorithm
- Enable/Disable — Temporarily suspend keys without revoking
- Revocation — Permanently invalidate keys with audit trail
- Key rotation — Seamlessly replace keys with linked history
- Auto lastUsedAt tracking — Know when keys were last used
- Global scopes —
["read", "write", "admin"] - Resource-specific scopes — Fine-grained per-resource permissions
- ResourceBuilder — Fluent API for building complex permissions
- Tags/Labels — Categorize keys for filtering
- Names & Descriptions — Human-readable identification
- Audit logging — Track all key lifecycle events
- Log querying — Filter by action, date, owner, key
- Log statistics — Aggregated insights
- Memory, Redis, Drizzle, Prisma, Kysely, Convex
- Memory cache, Redis cache, Custom cache adapters
Support human-readable duration strings instead of manual ISO timestamps.
// Before (current)
const { key } = await keys.create({
ownerId: "user_123",
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
})
// After (proposed)
const { key } = await keys.create({
ownerId: "user_123",
expiresIn: "30d" // or "1h", "7d", "90d", "1y"
})Simplify common scope patterns with presets.
// Before
const { key } = await keys.create({
ownerId: "user_123",
scopes: ["read"]
})
// After (proposed)
const { key } = await keys.createReadOnly({ ownerId: "user_123" })
const { key } = await keys.createFullAccess({ ownerId: "user_123" })Fluent API for common verification patterns.
// Before
const result = await keys.verify(headers)
if (!result.valid) throw new Error(result.error)
if (!keys.hasScope(result.record!, "write")) throw new Error("Forbidden")
// After (proposed)
const record = await keys
.verify(headers)
.requireScope("write")
.orThrow()Direct metadata updates without full rotation.
// Proposed
await keys.update(keyId, {
name: "Renamed Key",
tags: ["production", "critical"],
scopes: ["read", "write", "deploy"]
})Efficient batch operations for enterprise use cases.
// Proposed
const results = await keys.createMany([
{ ownerId: "user_1", scopes: ["read"] },
{ ownerId: "user_2", scopes: ["write"] }
])
await keys.revokeMany(["key_1", "key_2", "key_3"])
const records = await keys.verifyMany([key1, key2, key3])Protect APIs from abuse with built-in rate limiting that ties into key verification.
const keys = createKeys({
rateLimit: {
enabled: true,
default: { requests: 1000, window: "1h" }
}
})
// Per-key limits
const { key } = await keys.create({
ownerId: "user_123",
rateLimit: { requests: 100, window: "1m" }
})
// Verification includes rate limit check
const result = await keys.verify(apiKey)
if (result.rateLimited) {
return { error: "Rate limit exceeded", retryAfter: result.retryAfter }
}First-class support for popular frameworks.
// Express
import { createKeysMiddleware } from "keypal/express"
app.use("/api", createKeysMiddleware(keys, {
scopes: ["api:access"],
onError: (err, req, res) => res.status(401).json({ error: err.message })
}))
// Hono
import { keysMiddleware } from "keypal/hono"
app.use("/api/*", keysMiddleware(keys))
// Next.js
import { withApiKey } from "keypal/next"
export const GET = withApiKey(keys, async (req, { record }) => {
return Response.json({ user: record.metadata.ownerId })
})Real-time notifications for key lifecycle events.
const keys = createKeys({
webhooks: {
url: "https://api.example.com/webhooks/keys",
secret: process.env.WEBHOOK_SECRET,
events: ["created", "revoked", "rotated", "expired"]
}
})
// Webhook payload
{
event: "key.revoked",
timestamp: "2025-01-15T10:30:00Z",
data: {
keyId: "key_abc123",
ownerId: "user_123",
revokedBy: "admin_456"
}
}Track API usage per key with configurable quotas.
const { key } = await keys.create({
ownerId: "user_123",
quota: {
limit: 10000,
period: "month",
onExceeded: "block" // or "warn", "throttle"
}
})
// Get usage stats
const usage = await keys.getUsage(keyId)
// { used: 4521, limit: 10000, remaining: 5479, resetsAt: "2025-02-01" }
// Track custom usage
await keys.trackUsage(keyId, {
endpoint: "/api/generate",
tokens: 1500, // for AI APIs
cost: 0.002
})Allowlist or blocklist IP addresses per key.
const { key } = await keys.create({
ownerId: "user_123",
allowedIPs: ["192.168.1.0/24", "10.0.0.1"],
// or
blockedIPs: ["1.2.3.4"]
})
// Verification checks IP
const result = await keys.verify(apiKey, { ip: req.ip })
if (result.errorCode === "IP_NOT_ALLOWED") {
// Handle blocked IP
}Built-in support for environment separation.
const keys = createKeys({
environments: {
live: { prefix: "sk_live_" },
test: { prefix: "sk_test_", rateLimit: null }
}
})
// Create environment-specific keys
const liveKey = await keys.create({ ownerId: "user_123", env: "live" })
const testKey = await keys.create({ ownerId: "user_123", env: "test" })
// Auto-detect environment from key prefix
const result = await keys.verify(key)
console.log(result.record?.env) // "live" or "test"Declarative access control policies.
const keys = createKeys({
policies: {
"api:admin": {
scopes: ["*"],
resources: ["*"],
rateLimit: null
},
"api:readonly": {
scopes: ["read"],
maxTtl: "90d"
},
"api:integration": {
scopes: ["read", "write"],
requireIP: true,
maxResources: 10
}
}
})
const { key } = await keys.create({
ownerId: "user_123",
policy: "api:integration",
allowedIPs: ["10.0.0.1"]
})Support for organization-level key management.
const { key } = await keys.create({
ownerId: "user_123",
orgId: "org_456",
scopes: ["org:read", "org:write"]
})
// List all keys for an organization
const orgKeys = await keys.listByOrg("org_456")
// Revoke all keys when user leaves org
await keys.revokeByOwnerAndOrg("user_123", "org_456")Cryptographic request verification for sensitive operations.
// Client side
const signature = keys.sign({
method: "POST",
path: "/api/transfer",
body: { amount: 100 },
timestamp: Date.now()
}, secretKey)
// Server side
const isValid = await keys.verifySignature(request, signature, {
maxAge: "5m" // Prevent replay attacks
})Limit key usage by geographic region.
const { key } = await keys.create({
ownerId: "user_123",
allowedRegions: ["US", "EU", "GB"],
// or
blockedRegions: ["CN", "RU"]
})
const result = await keys.verify(apiKey, {
geo: { country: "US", region: "CA" }
})Child keys that inherit permissions from parent keys.
// Create a parent key with full access
const parent = await keys.create({
ownerId: "user_123",
scopes: ["read", "write", "admin"]
})
// Create child keys with restricted subsets
const child = await keys.create({
ownerId: "user_123",
parentId: parent.record.id,
scopes: ["read"] // Must be subset of parent
})
// Revoking parent automatically revokes children
await keys.revoke(parent.record.id, { cascade: true })Reusable templates for common key configurations.
keys.defineTemplate("external-partner", {
scopes: ["read"],
expiresIn: "90d",
rateLimit: { requests: 100, window: "1h" },
policy: "api:readonly"
})
const { key } = await keys.createFromTemplate("external-partner", {
ownerId: "partner_123",
name: "Acme Corp Integration"
})Migration support for moving between systems.
// Export keys (hashes only, never plaintext)
const exported = await keys.export({
ownerId: "user_123",
format: "json"
})
// Import from another system
await keys.import(exported, {
onConflict: "skip" // or "overwrite", "error"
})Time-based automated key management.
const { key } = await keys.create({
ownerId: "user_123",
schedule: {
disableAt: "2025-06-01T00:00:00Z",
revokeAt: "2025-07-01T00:00:00Z",
notifyBefore: "7d" // Webhook notification
}
})Auto-generate descriptions based on key usage patterns.
const insights = await keys.analyze(keyId)
// {
// suggestedName: "Production API - High Traffic",
// usagePattern: "Mainly read operations, peak at 2pm UTC",
// securityScore: 85,
// recommendations: ["Consider IP restriction", "Add rate limit"]
// }| Feature | Impact | Effort | Priority |
|---|---|---|---|
| Rate Limiting | 🔥🔥🔥 | Medium | P0 |
| Middleware (Express/Hono/Next) | 🔥🔥🔥 | Low | P0 |
| expiresIn helper | 🔥🔥 | Low | P0 |
| Usage Analytics & Quotas | 🔥🔥🔥 | High | P1 |
| IP Restrictions | 🔥🔥🔥 | Medium | P1 |
| Webhooks | 🔥🔥 | Medium | P1 |
| Environment Keys | 🔥🔥 | Low | P1 |
| Key Policies | 🔥🔥 | Medium | P2 |
| Update Metadata | 🔥🔥 | Low | P2 |
| Bulk Operations | 🔥🔥 | Medium | P2 |
| Multi-tenancy | 🔥🔥 | High | P2 |
| Request Signing | 🔥 | Medium | P3 |
| Key Templates | 🔥 | Low | P3 |
| Chainable Verification | 🔥 | Medium | P3 |
- Zero-config defaults — Everything works out of the box
- Progressive complexity — Simple things simple, complex things possible
- Type-safe — Full TypeScript inference
- Framework agnostic — Core has no dependencies on web frameworks
- Storage agnostic — Bring your own database
- Composable — Features work independently or together