Overview
API keys enable programmatic access to the platform for external integrations, CI/CD pipelines, and third-party tools. Keys support granular permissions through access levels, feature access, and scope validation.
Secret Keys (sk_live_)
For server-side integrations. Full feature access based on configuration. No origin restrictions. Never expose in client code.
Publishable Keys (pk_live_)
For client-side use. Requires allowedOrigins configuration. Routes must explicitly allow with allowPublicKeys.
Security Principles
- Least privilege: featureAccess requires explicit selection (min 1 feature)
- Key type separation: sk_live_ vs pk_live_ prefixes
- Origin validation: PUBLISHABLE keys require allowedOrigins
- Access level enforcement: READ_ONLY / READ_WRITE / FULL
- RLS defense-in-depth: Data filtered at database level
Quick Start
Key Types
SECRET vs PUBLISHABLE key types.
// API Key Types ┌─────────────────────────────────────────────────────────────────┐ │ SECRET Keys (sk_live_...) │ ├─────────────────────────────────────────────────────────────────┤ │ • For server-side integrations │ │ • CI/CD pipelines, backend services │ │ • Full feature access based on configuration │ │ • No origin restrictions │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ PUBLISHABLE Keys (pk_live_...) │ ├─────────────────────────────────────────────────────────────────┤ │ • For client-side use (can be exposed in browser) │ │ • REQUIRES allowedOrigins configuration │ │ • REQUIRES Origin header in requests │ │ • Routes must explicitly allow with allowPublicKeys: true │ └─────────────────────────────────────────────────────────────────┘
Patterns
Access Levels
Control what operations a key can perform.
// Access Level Hierarchy
┌─────────────────────────────────────────────────────────────────┐
│ FULL > READ_WRITE > READ_ONLY │
└─────────────────────────────────────────────────────────────────┘
// Access Level Capabilities
┌─────────────┬─────────┬────────────────┬──────────┐
│ Level │ GET │ POST/PUT/PATCH │ DELETE │
├─────────────┼─────────┼────────────────┼──────────┤
│ READ_ONLY │ ✓ │ │ │
│ READ_WRITE │ ✓ │ ✓ │ │
│ FULL │ ✓ │ ✓ │ ✓ │
└─────────────┴─────────┴────────────────┴──────────┘
// In route handlers
export const GET = withApiLogging(
withAuth({ scope: 'project' }, handler) // No access level = read
);
export const POST = withApiLogging(
withAuth({ scope: 'project', requiredAccess: 'write' }, handler)
);
export const DELETE = withApiLogging(
withAuth({ scope: 'project', requiredAccess: 'full' }, handler)
);Feature Access
Grant access to specific platform features.
// Feature Access Configuration
// Keys REQUIRE at least one feature (enforced by validation)
// Must explicitly grant access to each feature when creating
interface ValidatedApiKey {
// ...
featureAccess: string[]; // e.g., ['projects', 'resources', 'feedback']
}
// In route handlers - require specific features
export const GET = withApiLogging(
withAuth(
{ scope: 'project', requiredFeatures: ['projects', 'resources'] },
async (request, { auth }) => {
// Only keys with BOTH features can access
}
)
);
// Missing features return 403
{
"ok": false,
"error": {
"code": "FORBIDDEN",
"message": "API key missing required feature access: resources"
}
}Scope Validation
Program-scoped vs project-scoped keys.
// Key Scope Validation
// Keys are scoped to programs OR specific projects
// Program-scoped key
apiKey.programId = 'prog_123';
apiKey.projectId = null; // Can access ANY project in program
// Project-scoped key
apiKey.programId = 'prog_123';
apiKey.projectId = 'proj_456'; // Can ONLY access this project
// withAuth validates scope automatically
export const GET = withApiLogging(
withAuth({ scope: 'project' }, async (request, { auth, projectId }) => {
// For project-scoped routes:
// - Project-scoped key: must match projectId
// - Program-scoped key: project must be in that program
})
);
export const GET = withApiLogging(
withAuth({ scope: 'program' }, async (request, { auth, programId }) => {
// For program-scoped routes:
// - Key's programId must match
})
);Origin Validation
PUBLISHABLE key origin restrictions.
// Origin Validation (PUBLISHABLE Keys)
// Key configuration
apiKey.keyType = 'PUBLISHABLE';
apiKey.allowedOrigins = [
'https://app.example.com', // Exact match
'.example.com' // Suffix match (*.example.com)
];
// Request MUST include Origin header
// Example request from browser:
fetch('/api/v1/projects', {
headers: {
'X-API-Key': 'pk_live_abc123...',
// Origin header is set automatically by browser
}
});
// Routes must explicitly allow publishable keys
export const GET = withApiLogging(
withAuth(
{ scope: 'program', allowPublicKeys: true },
handler
)
);
// Without allowPublicKeys, publishable keys get 403:
{
"ok": false,
"error": {
"code": "FORBIDDEN",
"message": "Publishable keys not allowed on this endpoint"
}
}Integration Examples
Using API keys in external integrations.
// Using API keys in integrations
// Server-side (Node.js, Python, etc.)
const response = await fetch('https://api.centercode.com/api/v1/projects', {
headers: {
'X-API-Key': 'sk_live_abc123...',
'Content-Type': 'application/json',
},
});
// Client-side (with publishable key)
// ONLY for endpoints with allowPublicKeys: true
const response = await fetch('https://api.centercode.com/api/v1/public-data', {
headers: {
'X-API-Key': 'pk_live_xyz789...',
},
});
// curl example
curl -X GET 'https://api.centercode.com/api/v1/programs' \
-H 'X-API-Key: sk_live_abc123...'
// With request body
curl -X POST 'https://api.centercode.com/api/v1/projects' \
-H 'X-API-Key: sk_live_abc123...' \
-H 'Content-Type: application/json' \
-d '{"name": "New Project", "programId": "..."}'Watch Out
Never log or expose raw API keys
Don't
// Exposing API key in logs/responses (WRONG)
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 (CORRECT)
logger.info('API key used', {
keyId: apiKey.id,
keyPrefix: 'sk_live_abc...' // Only log prefix
});
// Only show prefix in UI
return NextResponse.json({
ok: true,
data: {
id: apiKey.id,
prefix: 'sk_live_abc...',
name: apiKey.name,
createdAt: apiKey.createdAt
}
});Always require features for protected routes
Don't
// Assuming key has feature access (WRONG)
export const GET = withApiLogging(
withAuth({ scope: 'project' }, async (request, { auth }) => {
// Keys with empty featureAccess can reach here!
// They shouldn't access project features
const data = await services.getProjectData();
})
);Do
// Explicitly require features (CORRECT)
export const GET = withApiLogging(
withAuth(
{ scope: 'project', requiredFeatures: ['projects'] },
async (request, { auth }) => {
// Only keys with 'projects' feature can access
const data = await services.getProjectData();
}
)
);- Keys require at least one feature - always verify routes check requiredFeatures
- PUBLISHABLE keys require allowedOrigins configuration
- Never store API keys in plaintext - always hash
- Always set key expiration for enhanced security