Workspace API Key Management
Section titled “Workspace API Key Management”This document describes the workspace-scoped API key system that integrates with Better Auth and provides public API authentication.
Overview
Section titled “Overview”Workspace administrators can create and manage API keys for authenticating requests to the public API (api.legaciti.org). Each workspace maintains its own set of API keys, and keys are tied to a single domain per the system design.
Architecture
Section titled “Architecture”Key Components
Section titled “Key Components”- Better Auth API Key Plugin (
apiKeyplugin in auth config)- Manages API key creation, storage, and retrieval
- Stores keys in the
apikeytable with plugin-managed fields
- Configuration ID:
worker-consumer-api-consumer(identifies public API keys)- References:
organization(workspace ID as reference_id)
- References:
-
Dashboard Server Functions (
apps/dashboard/src/server/functions/workspace-settings/index.ts)listWorkspaceApiKeys(): Retrieve all API keys for a workspacecreateWorkspaceApiKey(): Generate new API key with name, domain, optional rate limitsdeleteWorkspaceApiKey(): Hard delete an API key
-
Public API Worker (
workers/consumer-api/src/index.ts)- Validates incoming API keys against Better Auth
apikeytable - Enforces domain restrictions
- Applies rate limiting (plugin-native
remainingcounter) - Checks workspace active/termination status
- Validates incoming API keys against Better Auth
-
Dashboard Feature Layer (
apps/dashboard/src/features/workspace-settings/)- React Query hooks for fetching, creating, deleting API keys
- Type-safe wrappers around server functions
- UI components in dashboard (ApiKeyList, GenerateApiKeyDialog)
Database Schema
Section titled “Database Schema”apikey table (Better Auth plugin)
Section titled “apikey table (Better Auth plugin)”| Column | Type | Notes |
|---|---|---|
id | TEXT PRIMARY KEY | Unique key identifier |
config_id | TEXT | Config ID; worker-consumer-api-consumer for public API |
name | TEXT | Human-readable name (optional) |
key | TEXT | Plaintext API key (Better Auth stores plaintext) |
reference_id | TEXT | Workspace/organization ID |
enabled | INTEGER | 0 or 1; can be disabled without deletion |
expires_at | INTEGER | Expiration timestamp (milliseconds); NULL = never expires |
metadata | TEXT | JSON metadata; stores { domain: string } |
rate_limit_enabled | INTEGER | 0 or 1 |
rate_limit_max | INTEGER | Max requests per window |
rate_limit_time_window | INTEGER | Time window in seconds |
remaining | INTEGER | Requests remaining in current window |
last_request | INTEGER | Timestamp of last request |
created_at | INTEGER | Creation timestamp |
updated_at | INTEGER | Last update timestamp |
Indexes
Section titled “Indexes”idx_apikey_key(UNIQUE): Fast plaintext key lookupidx_apikey_reference_id: Fast workspace lookupidx_apikey_config_id: Fast config filtering
Creating an API Key (Dashboard UI)
Section titled “Creating an API Key (Dashboard UI)”- Navigate to Workspace Settings → API Keys
- Click Generate New Key
- Enter:
- Name: Human-readable identifier (e.g., “Production Client”)
- Domain: Single domain this key can be used from
- Optionally set rate limits:
- Max Requests: Limit per time window
- Time Window: Duration in seconds
- Click Create
- Important: Copy the plaintext key immediately. It will not be displayed again.
Using an API Key
Section titled “Using an API Key”Call the public API with the X-API-Key header:
curl -H "X-API-Key: lcg_prod_abc123xyz789" \ https://api.legaciti.org/v1/publicationsDeleting an API Key
Section titled “Deleting an API Key”- Navigate to Workspace Settings → API Keys
- Click Delete (trash icon) on the key you want to remove
- Confirm deletion in the dialog
- The key is immediately hard-deleted and cannot be recovered
Managing Rate Limits
Section titled “Managing Rate Limits”If rate limiting is enabled on a key:
- The
remainingcounter decrements with each request - When
remainingreaches 0, requests are rejected with 429 status - The counter resets on the next window boundary (if configured for windows > 1)
If rate limiting is disabled:
- Requests have no per-key limit (subject to global IP-based rate limiting)
Security Model
Section titled “Security Model”Authorization
Section titled “Authorization”- Only workspace_admin or superadmin can manage API keys for a workspace
- Regular workspace members cannot create, list, or delete keys
Key Secrecy
Section titled “Key Secrecy”- Plaintext keys are displayed once only after creation
- Keys are never shown again in the dashboard
- Always copy the key immediately to a secure location (e.g., environment variables)
- There is no “show/reveal” button in the dashboard
Domain Restriction
Section titled “Domain Restriction”- Each key is restricted to a single domain (single-domain contract)
- Domain stored in
metadataJSON:{ domain: "api.example.com" } - Public API worker enforces domain match for future use
Workspace Lifecycle
Section titled “Workspace Lifecycle”- API keys are only active if the workspace has status
active - Keys are blocked if workspace has been archived or is past termination date
- Use workspace settings to manage archive/termination status
API Endpoints (Server Functions)
Section titled “API Endpoints (Server Functions)”listWorkspaceApiKeys
Section titled “listWorkspaceApiKeys”Method: GET Authorization: workspace_admin or superadmin
Response:
{ keys: [ { id: string; name: string | null; start: string | null; // "lcg" prefix: string | null; // e.g., "prod" enabled: boolean; created_at: number; // milliseconds expires_at: number | null; // milliseconds last_request: number | null; // milliseconds domain: string | null; // extracted from metadata } ]}createWorkspaceApiKey
Section titled “createWorkspaceApiKey”Method: POST Authorization: workspace_admin or superadmin
Request:
{ name: string; // Min 1, max 120 chars domain: string; // Min 1, max 255 chars expiresInSeconds?: number; // Optional rateLimitMax?: number; // Optional rateLimitTimeWindow?: number; // Optional, in seconds}Response:
{ key: string; // Plaintext key (one-time display) api_key: string; // Same as key api_key_id: string; // Database ID for reference name: string; domain: string; // Normalized created_at: number; // milliseconds expires_at: number | null; // milliseconds start: string | null; // e.g., "lcg" prefix: string | null; // e.g., "prod_key"}deleteWorkspaceApiKey
Section titled “deleteWorkspaceApiKey”Method: POST Authorization: workspace_admin or superadmin
Request:
{ id: string; // API key ID to delete}Response:
{ error?: string; // If authorization denied}Side Effect: Key is hard-deleted immediately. No recovery possible.
Public API Worker Validation
Section titled “Public API Worker Validation”When a request arrives at /v1/publications or /api/ingest with X-API-Key header:
- Lookup: Query
apikeytable forconfig_id='worker-consumer-api-consumer' AND key=? - Enable Check: Verify
enabled=1 - Expiration Check: Verify
expires_at IS NULL OR expires_at > now() - Workspace Check: If
reference_idpresent, verify workspace is active + not past termination - Rate Limit Check: If
rate_limit_enabled=1andremaining <= 0, return 429 - Update: Set
last_request=now()and decrementremaining(if rate limit enabled) - Allow: Request proceeds
All checks fail with appropriate HTTP status codes (401, 403, 429).
Testing
Section titled “Testing”Unit Tests
Section titled “Unit Tests”Run unit tests for server functions and UI components:
pnpm --filter @apps/dashboard test -- api-keys.test.tspnpm --filter @apps/dashboard test -- api-keys-components.test.tsxRun worker integration tests:
pnpm --filter @workers/consumer-api test -- api-key-integration.test.tsE2E Testing
Section titled “E2E Testing”End-to-end tests exercise the full flow: create a key, use it to call the public API, verify rate limiting, delete the key.
Tests are located in tests/features/ and run via the custom Node harness.
Migration from Legacy System
Section titled “Migration from Legacy System”The legacy system used hashed keys in a separate api_keys table. The new system:
- Uses Better Auth plugin for key management
- Stores plaintext keys (plugin design)
- Adds domain-level restriction
- Implements plugin-native rate limiting (instead of request logs)
- Removes per-key request logging
Rollout Window
Section titled “Rollout Window”During rollout, both systems may be active:
- New keys are created via Better Auth plugin
- Existing keys in legacy
api_keystable are still functional - After rollout complete, legacy
api_keystable can be deprecated
Key Migration Notes
Section titled “Key Migration Notes”- Existing hashed keys cannot be migrated (plaintext unavailable)
- Consumers must rotate keys: generate new keys in dashboard, update clients
- Legacy key operations (via old API) continue until cutoff date
- Provide migration window (e.g., 60 days) before disabling legacy keys
Future Enhancements
Section titled “Future Enhancements”- Key rotation (automatic expiration + new key generation)
- Per-domain analytics (requests per domain)
- Scope-based permissions (e.g., read-only publications vs. ingest)
- Multiple domains per key
- API key webhooks (events when key is used, rotated, deleted)