Overview
API keys provide programmatic access to the platform for external integrations, CI/CD pipelines, and third-party tools. Keys are scoped to programs or projects and support rotation without downtime.
What it is
A system for generating, managing, and validating API keys that authenticate programmatic access to REST endpoints.
Why we use it
Enable external integrations, support automation workflows, and provide granular access control for third-party tools.
When to use
When external systems need to access platform data, for CI/CD integrations, or when users need machine-to-machine authentication.
Key Features
- Scoped access - keys belong to programs or projects
- Key rotation without downtime via rotate endpoint
- Automatic audit logging of all API key operations
- Rate limiting per key to prevent abuse
Quick Start
Creating and Using API Keys
Generate a key via the UI or API, then include it in the X-API-Key header.
// Creating and using API keys
import { createApiKey, verifyApiKey } from '@/features/api-keys';
// Generate a new API key for a program
const { key, apiKey } = await createApiKey({
name: 'CI/CD Pipeline',
programId: 'prog_123',
expiresAt: null, // No expiration
}, actor);
// The raw key is only returned once - store it securely!
// key = "ak_live_abc123..."
// Verify an API key in a route handler
const apiKeyRecord = await verifyApiKey(rawKey);
if (!apiKeyRecord) {
throw new AuthenticationError('Invalid API key');
}
// Access is now authenticated for the key's program/project scopePatterns
API Key Authentication
Validate keys in route handlers using verifyApiKey().
// API key authentication in route handlers
import { verifyApiKey } from '@/features/api-keys';
import { AuthenticationError } from '@/lib/errors';
export async function GET(request: NextRequest) {
// Check for API key in header
const apiKeyHeader = request.headers.get('X-API-Key');
if (!apiKeyHeader) {
throw new AuthenticationError('API key required');
}
// Verify the key and get its scope
const apiKey = await verifyApiKey(apiKeyHeader);
if (!apiKey) {
throw new AuthenticationError('Invalid API key');
}
// Key is valid - use apiKey.programId or apiKey.projectId
// for scope-based data access
const data = await repository.findByProgram(apiKey.programId);
return NextResponse.json({ ok: true, data });
}Key Scopes
Keys are scoped to programs or projects with appropriate access levels.
// API key scopes
import { createApiKey } from '@/features/api-keys';
// Program-scoped key (access to all projects in program)
const programKey = await createApiKey({
name: 'Program Integration',
programId: 'prog_123',
projectId: null, // No project restriction
}, actor);
// Project-scoped key (access to single project only)
const projectKey = await createApiKey({
name: 'Project CI/CD',
programId: 'prog_123',
projectId: 'proj_456', // Restricted to this project
}, actor);
// Verify scope in route handlers
const apiKey = await verifyApiKey(rawKey);
if (apiKey.projectId) {
// Key is project-scoped
await verifyProjectAccess(apiKey.projectId, resourceId);
} else {
// Key is program-scoped
await verifyProgramAccess(apiKey.programId, resourceId);
}Key Rotation
Rotate keys without downtime - old key remains valid briefly during transition.
// Key rotation without downtime
import { rotateApiKey } from '@/features/api-keys';
// Rotate generates a new key while keeping the old one
// valid for a brief transition period
const { key: newKey, apiKey } = await rotateApiKey(
keyId,
actor
);
// The old key remains valid during rotation window
// Client can switch to new key without downtime
// New key is returned - update your integration
// newKey = "ak_live_xyz789..."
// Audit log captures the rotation event
// action: 'apiKey.rotate'Watch Out
Never log or expose API keys in responses
Don't
// Exposing API key in logs/responses (DON'T)
logger.info('API key used', {
apiKey: rawKey // NEVER log the raw key!
});
return NextResponse.json({
ok: true,
apiKey: rawKey // NEVER return raw key!
});Do
// Properly masked key handling
logger.info('API key used', {
keyId: apiKey.id,
keyPrefix: apiKey.prefix // Only log the prefix
});
// Only show prefix in UI
return NextResponse.json({
ok: true,
data: {
id: apiKey.id,
prefix: apiKey.prefix, // "ak_live_abc..."
name: apiKey.name,
createdAt: apiKey.createdAt
}
});Keys must be hashed with bcrypt, never stored in plaintext
Don't
// Storing key in plaintext (DON'T)
await prisma.apiKey.create({
data: {
key: rawKey, // NEVER store plaintext!
name: input.name,
}
});Do
// Properly hashed key storage
import { hashApiKey } from '@/features/api-keys';
const hashedKey = await hashApiKey(rawKey);
await prisma.apiKey.create({
data: {
keyHash: hashedKey, // Store bcrypt hash
prefix: rawKey.slice(0, 12), // Store prefix for display
name: input.name,
}
});- Consider setting key expiration for enhanced security
- Always apply rate limiting to API key endpoints