CentercodePorygon
DittoPorygonAPI

API Keys

Key types, permissions, and security

SafeReliable

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

Related

Authentication

Session + API key auth

Authorization

Role and permission checks

API Logging

Request tracking and audit