Create API Key Flow
Architecture documentation — API key creation flow, dual-key system, AES-256-GCM encryption, form validation, and database schema.
When a user creates an API key, the system generates a dual-key pair, encrypts the secret key with AES-256-GCM, and presents both keys in a one-time success screen. This document covers the full flow: UI interaction, backend processing, encryption, and database storage.
Overview
Users create API keys to authenticate image transformation requests. Each API key consists of a public key (pk_..., for identification) and a secret key (sk_..., for URL signing). The full public key can be viewed and copied from the API key list. The secret key is only shown once at creation or rotation time.
Image source domain restrictions are configured at the project level (in Settings → Domain Security), not per API key.
Flow Diagram
Key Concepts
API Key Structure
| Component | Format | Description |
|---|---|---|
| Public Key | pk_ + 16 bytes (base64url) = 25 chars | Public identifier, stored in plaintext. Used in URL ?key= param |
| Secret Key | sk_ + 32 bytes (base64url) | Used for HMAC-SHA256 URL signing, stored encrypted |
Encryption
| Aspect | Details |
|---|---|
| Algorithm | AES-256-GCM (authenticated encryption) |
| Key Derivation | HKDF (RFC 5869) from master secret |
| Storage | Only secretKey is encrypted; publicKey is stored in plaintext |
Form Fields
| Field | Required | Description |
|---|---|---|
| Key Name | Yes | Descriptive name (e.g., "Production", "Development") |
| Expiration | No | Optional expiry date for automatic key invalidation |
User Journey
1. Open Create Dialog
User navigates to a project's API Keys section and clicks the "Create API Key" button.
2. Fill Out Form
The dialog presents a form with:
- Key Name — Input field for a descriptive name
- Expiration — Select dropdown with preset options (30 days, 90 days, etc.)
3. Submit Form
When the user clicks "Create Key":
- Frontend validates inputs (name required)
- Sends request to
apiKey.createtRPC mutation - Button shows loading state during request
4. Backend Processing
The backend:
- Generates cryptographically secure publicKey and secretKey
- Encrypts secretKey using AES-256-GCM (publicKey is stored in plaintext)
- Stores values in database
- Returns publicKey and plaintext secretKey to client
Every mutation runs through protected tRPC procedures and project-level access verification before key operations are allowed.
5. Success Screen
Dialog transitions to success view showing:
- Secret Key — Full secret key with copy button and warning
- Public Key — Public identifier for URL
?key=parameter - Usage Example — Code snippet showing how to sign URLs
Important: The secret key is only shown once. Users must copy and save it securely.
The public key (
pk_...) can always be copied later from the API key list page (displayed masked with a copy button).
6. Complete
User clicks "Done" to close the dialog. The API key list automatically refreshes to show the new key.
Rotation Flow (Related)
Rotation uses a single DB transaction to revoke the old key and insert the replacement key atomically:
- Verify project access
- Revoke old key (
revokedAt = now) - Insert new key (inherits name, expiration, and per-key limit values)
- Invalidate old public-key cache (best effort)
- Return new plaintext secret once via UI dialog
Error Scenarios
| Scenario | Behavior |
|---|---|
| Empty key name | Submit button disabled |
| Network / server error | Dialog remains on form step; user can retry (no dedicated inline error banner yet) |
Form Validation Rules
| Field | Validation |
|---|---|
| Key Name | Required, non-empty after trim |
| Expiration | Optional, must be future date if set |
Related Files
| File | Purpose |
|---|---|
src/modules/project-detail/ui/components/create-api-key-dialog.tsx | Dialog component with form and success states |
src/modules/project-detail/ui/components/expiration-select.tsx | Expiration dropdown component |
src/server/lib/api-key.ts | Key generation and encryption functions |
src/server/api/routers/apiKey.ts | Backend tRPC router for API key operations |
src/modules/project-detail/ui/components/api-key-types.ts | TypeScript types for API key data |
Database Schema
The api_key table stores:
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
projectId | UUID | Foreign key to projects table |
name | String | User-defined key name |
publicKey | String | Public identifier (plaintext, unique, indexed) |
secretKey | String | Secret key (AES-256-GCM encrypted) |
expiresAt | Timestamp | Optional expiration date |
createdAt | Timestamp | Creation timestamp |
lastUsedAt | Timestamp | Last usage timestamp |
revokedAt | Timestamp | Revocation timestamp (null if active) |
Security Considerations
- Secret Key Display — Only shown once at creation or rotation, never retrievable again
- Public Key Display — Public key (
pk_...) is available in the API key list (masked display with copy button). Since it's a public identifier without signing capability, this is safe to expose - Encryption at Rest — Secret key encrypted using HKDF-derived AES-256-GCM; public key stored in plaintext
- Domain Restrictions — Image source domains are managed at the project level, shared across all keys
- Expiration — Optional automatic key invalidation
- Audit Trail —
createdAt,lastUsedAt,revokedAttimestamps
Related Documentation
- URL Signing — Request signing and authentication
- Domain Whitelisting — Settings → Domain Security
- Security Best Practices — Key management recommendations
- Control Plane and Multi-Tenancy — How key lifecycle fits tenancy and access control
- What is OptStuff? — Service overview
Last updated on
User Onboarding Flow
Architecture documentation — complete user onboarding journey from sign-up to first API key, including team creation, project setup, and documentation links strategy.
SSRF Prevention
How our image proxy architecture prevents Server-Side Request Forgery (SSRF) — the design flaw, the attack vector, and the fix.