Last Updated: 2026-02-28
Status: ✅ Fully Implemented (CRUD + Gateway Validation)
Overview
LogicSpike provides a full API key management system for machine-to-machine integrations and public SDK usage. Keys are workspace-scoped, SHA-256 hashed, and carry granular per-service permissions.
Architecture
| Layer | Responsibility | Files |
|---|---|---|
| Manager | CRUD routes for key lifecycle | apps/manager/src/routes/api-keys.ts, apps/manager/src/auth/api-key.ts |
| Gateway | Validates API keys on public routes | apps/gateway/src/middleware/api-key.middleware.ts |
| Dashboard | UI for key management | apps/seller-dashboard/src/hooks/use-api-keys.ts, apps/seller-dashboard/src/app/dashboard/settings/api-keys/ |
| Database | api_keys table in core-database |
packages/core-database/src/schema.ts |
Key Types
| Type | Prefix | Usage | Restrictions |
|---|---|---|---|
| Secret | ls_ |
Server-to-server integrations | Full read/write per permissions |
| Publishable | pk_ |
Browser SDKs, public clients | Read-only (GET/HEAD/OPTIONS), domain-locked |
Domain Locking (Publishable Keys)
Publishable keys can be restricted to specific domains via allowedDomains. When set:
- Browser requests must include an
Originheader matching one of the allowed domains - Requests without a matching origin are rejected with
403 Domain not allowed - Empty
allowedDomains= unrestricted (useful during development)
Key Generation & Storage
- Generation: 32-byte cryptographically random key via
crypto.getRandomValues() - Hashing: SHA-256 via Web Crypto API (
crypto.subtle.digest) - Storage: Only the hash is stored in DB; raw key returned once at creation
- Lookup: Gateway hashes incoming key and queries DB by
key_hash
Raw Key: ls_a1b2c3d4e5f6... (68 chars)
Hash: e3b0c44298fc1c14... (64 hex chars)Permission Model
Permissions are stored as a flat array of granular PBAC strings:
[
"blog:posts.read",
"blog:posts.write",
"media:files.read"
]The gateway middleware (api-key.middleware.ts) checks:
- Extract the
servicefrom the route (e.g.,apiKeyMiddleware("blog")). - Verify the key has at least one permission starting with
${service}:. - If the request modifies data (POST/PUT/DELETE), verify the key has at least one permission that does not end with
.reador:read. - Downstream microservices enforce exact string checks (e.g.,
requirePermission("blog:posts.write")).
API Endpoints
Base URL: /manager/api-keys (proxied through Gateway, requires JWT auth)
GET /
List all API keys for the current workspace.
- Auth: Bearer JWT
- Response: Array of keys (without raw key or hash)
POST /
Generate a new API key.
- Auth: Bearer JWT
- Body:
{ "name": "Production Blog SDK", "description": "Read-only access for blog widget", "permissions": ["blog:posts.read"], "expiresIn": "90d", "type": "publishable", "allowedDomains": ["https://mysite.com"] } - Response: Key object including raw
keyfield (shown only once)
PATCH /:id
Update a key's name or description.
- Auth: Bearer JWT
- Body:
{ "name": "New Name", "description": "..." }
DELETE /:id
Revoke a key (soft delete — sets status: "revoked").
- Auth: Bearer JWT
- Response:
{ "success": true }
Expiration Options
| Value | Duration |
|---|---|
30d |
30 days |
90d |
90 days |
1y |
1 year |
never |
No expiration |
The gateway checks expiration on every request. Expired keys return 403 API key expired.
Gateway Validation Flow
1. Extract key from x-api-key header or Authorization: Bearer
2. SHA-256 hash the key
3. DB lookup by key_hash
4. Check: status === "active"?
5. Check: not expired?
6. If publishable:
a. Reject non-GET methods (read-only)
b. Validate Origin against allowedDomains
7. Permission check: validates key has matching PBAC prefix strings for the target service and HTTP method.
8. Set context: { tenant_id, user_id: "system" }
9. Forward request to downstream serviceDatabase Schema
See schema_specification.md — Table #22 api_keys.
Known Gaps & TODOs
| Issue | Priority | Detail |
|---|---|---|
| No role guard on CRUD | 🟠 High | Any team member can create/revoke keys — should be Owner/Admin only |
| Hash function duplicated | 🟡 Medium | Same SHA-256 logic exists in both manager and gateway — extract to @repo/core-auth |
| No rate limiting on validation | 🟡 Medium | Brute-force key guessing possible without rate limiting |
x-api-key not in CORS headers |
🟡 Medium | Browser SDK clients can only use Authorization: Bearer, not x-api-key |
| Limit enforcement (Phase 7) | ⏳ Future | POST /api-keys should check platform.api_keys limit |
| Audit logging | ⏳ Future | Key create/revoke events should be logged per audit_log_spec.md |