Skip to content

Granular API Key Access Control

A single API key with full access is a liability. ZeptoDB now supports per-key permissions — restrict which symbols and tables a key can access, bind keys to tenants, set expiry dates, and edit keys in place without rotation.


Symbol/Table ACLs

Each key can be scoped to specific symbols and tables at creation time.

Tenant Binding

Keys can be bound to a tenant_id for multi-tenant isolation.

Key Expiry

Keys have an optional expiry timestamp — expired keys are rejected on validation.

In-Place Editing

PATCH endpoint to modify key fields without revoking and re-creating.


The ApiKeyEntry was extended with new fields while maintaining backward compatibility with the existing file format:

struct ApiKeyEntry {
std::string id;
std::string hashed_key;
Role role;
bool enabled;
int64_t created_at_ns;
int64_t last_used_ns;
std::vector<std::string> allowed_symbols; // empty = all
std::vector<std::string> allowed_tables; // empty = all
std::string tenant_id; // empty = no binding
int64_t expires_at_ns; // 0 = never
bool is_expired() const;
};

The file format appends tenant_id (field 8) and expires_at_ns (field 9) — older files load cleanly with defaults.


API Request with key
┌─────────────┐
│ Key lookup │──→ not found → 401
└──────┬──────┘
┌─────────────┐
│ is_expired()?│──→ yes → 401 "key expired"
└──────┬──────┘
┌─────────────┐
│ enabled? │──→ no → 401 "key disabled"
└──────┬──────┘
┌─────────────┐
│ Symbol ACL │──→ denied → 403
└──────┬──────┘
┌─────────────┐
│ Table ACL │──→ denied → 403
└──────┬──────┘
✅ Authorized

{
"role": "writer",
"symbols": ["AAPL", "MSFT"],
"tables": ["trades", "quotes"],
"tenant_id": "desk-alpha",
"expires_at_ns": 1743465600000000000
}

Response now includes scope, tenant, expiry, and last-used timestamps:

FieldDescription
allowed_symbolsSymbol whitelist (empty = unrestricted)
allowed_tablesTable whitelist (empty = unrestricted)
tenant_idBound tenant (empty = global)
expires_at_nsExpiry timestamp (0 = never)
last_used_nsLast successful validation time

Modify any combination of fields in place:

{
"symbols": ["AAPL", "MSFT", "GOOG"],
"tenant_id": "desk-beta",
"enabled": false
}

This avoids the revoke-and-recreate pattern that forces downstream credential updates.


When a key has tenant_id set, all queries executed with that key are scoped to the tenant’s data. Combined with symbol ACLs, this enables fine-grained access patterns:

Use CaseConfig
Trading desk sees only their symbolssymbols: ["AAPL","MSFT"], tenant_id: "desk-1"
Read-only analytics keyrole: reader, tables: ["trades"]
Temporary contractor accessexpires_at_ns: <30 days>, symbols: ["*"]
Service account (no expiry)expires_at_ns: 0, role: writer

TestWhat It Verifies
ExpiryBlocksValidationExpired key is rejected
NonExpiredKeyValidatesFuture-expiry key accepted, expires_at_ns preserved
TenantIdPreservedTenant ID round-trips through create/validate
UpdateKeyFieldsSymbols and tenant updated via update_key
UpdateKeyDisableKey disabled via update_key
TenantAndExpiryPersistenceAll new fields survive file persistence