Skip to content

API Key Authentication

The Eryxon Flow API uses API keys for external integrations (ERP systems, automation, etc.). Keys follow the format ery_live_xxx or ery_test_xxx.

  1. Admin creates API key via dashboard (Admin > API Keys)
  2. System generates random key: ery_live_<32-random-chars>
  3. Key is hashed using SHA-256 and stored in api_keys table
  4. Plaintext key shown once to user (never stored)
  1. Client sends request with Authorization: Bearer ery_live_xxx
  2. System extracts key prefix (first 12 chars) for efficient lookup
  3. Looks up candidate keys by prefix
  4. Hashes provided key with SHA-256 and compares to stored hash
  5. On match: sets tenant context and allows request
┌─────────────────────┐ ┌──────────────────────┐
│ Dashboard User │ │ External System │
│ (JWT Auth) │ │ (API Key Auth) │
└─────────┬───────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ api-key-generate │ │ api-jobs, api-parts │
│ (creates keys) │ │ (validates keys) │
└─────────┬───────────┘ └──────────┬───────────┘
│ │
│ SHA-256 hash │ SHA-256 hash
▼ ▼
┌──────────────────────────────────────────────────┐
│ api_keys table │
│ key_prefix | key_hash | tenant_id | active │
└──────────────────────────────────────────────────┘

All API endpoints use the shared authentication module:

supabase/functions/_shared/auth.ts
import { authenticateAndSetContext } from "../_shared/auth.ts";
// In your endpoint:
const { tenantId } = await authenticateAndSetContext(req, supabase);

This module:

  • Extracts Bearer token from Authorization header
  • Validates key format (ery_live_* or ery_test_*)
  • Looks up key by prefix (efficient query)
  • Hashes and compares using SHA-256
  • Sets tenant context for Row-Level Security
  • Updates last_used_at timestamp (async)
PatternEndpointsToken TypeUse Case
API Keyapi-jobs, api-parts, etc.ery_live_xxxExternal integrations
JWTapi-key-generate, api-integrationsSupabase sessionDashboard users
ery_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
└─┬─┘└┬─┘└──────────────┬────────────────┘
│ │ │
│ │ └── 32 random chars
│ └── Environment (live/test)
└── Prefix identifier
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
name TEXT NOT NULL,
key_prefix TEXT NOT NULL, -- First 12 chars for lookup
key_hash TEXT NOT NULL, -- SHA-256 hash of full key
active BOOLEAN DEFAULT true,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
created_by UUID REFERENCES profiles(id)
);
-- Index for efficient prefix lookup
CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix) WHERE active = true;
  1. Keys are never stored - Only SHA-256 hash is persisted
  2. Prefix lookup - Avoids comparing all keys (O(1) vs O(n))
  3. Constant-time comparison - SHA-256 comparison prevents timing attacks
  4. Async last_used update - Doesn’t block request response
  5. Soft delete - Keys are deactivated, not deleted (audit trail)
ErrorHTTP StatusCause
Missing or invalid authorization header401No Bearer token
Invalid API key format401Key doesn’t match ery_* pattern
Invalid API key401Key not found or hash mismatch
Terminal window
curl -H "Authorization: Bearer ery_live_yourkey" \
https://yourproject.supabase.co/functions/v1/api-jobs
curl -H "Authorization: Bearer invalid_key" \
https://yourproject.supabase.co/functions/v1/api-jobs