Designing API Key Auth for a Multi-Product Developer Platform
API key authentication seems simple until you’re designing it for a platform with multiple products, multiple environments, and two distinct types of API consumer.
smplkit serves both management operations (create a service, invite a user, update a config) and runtime operations (fetch a config value, evaluate a feature flag). These two audiences have fundamentally different needs, and trying to serve both with a single key type creates problems.
Here’s how we designed the key model and why.
Two Key Types
smplkit uses two distinct key types:
API keys (sk_live_...) are for management and control plane operations. They’re account-scoped — not tied to any specific environment. A CI/CD pipeline uses an API key to create environments, manage services, or update config values. API keys are managed from a top-level API Keys page in the console.
SDK keys (sdk_live_...) are for runtime data plane operations. They’re environment-scoped — each key is tied to a specific environment (production, staging, development). A customer’s application uses an SDK key to fetch config values or evaluate feature flags at runtime. SDK keys are managed from within an environment’s detail page.
The prefix makes the key type identifiable by inspection. If a developer accidentally puts an SDK key in a CI/CD pipeline configuration (or vice versa), the error is visible just from looking at the key format.
Why Two Types
The scoping requirements are different:
Management operations are inherently account-level. Creating a service, inviting a user, or listing environments doesn’t belong to any specific environment. An account-scoped key makes sense because the operations don’t have environment context.
Runtime operations are inherently environment-scoped. Fetching a config value in production must return production values, not staging values. The whole point of environments is isolation, and the SDK key enforces that isolation at the authentication level.
Forcing a single key type to serve both creates awkward tradeoffs. An account-scoped key used for runtime access bypasses environment isolation. An environment-scoped key used for management requires picking an arbitrary environment for account-level operations.
The Console Mirrors the Scoping
API keys appear on the top-level API Keys page — because they’re account-scoped and don’t belong under any environment. SDK keys appear on the environment detail page — because they’re environment-scoped and belong to a specific environment.
This isn’t just UI organization. It’s a teaching mechanism. A developer who sees SDK keys inside the Production environment page immediately understands the scoping relationship. A developer who sees API keys at the top level understands those keys span the entire account.
Industry Precedent
This model isn’t novel. It’s drawn from how mature developer platforms handle the same problem:
Established feature flag platforms use SDK keys (per-environment, for flag evaluation) and API access tokens (account-level, for management). Stripe has publishable keys (client-side, limited scope) and secret keys (server-side, full access) with separate test and live modes. Major observability platforms use API keys and application keys with different permission scopes.
The specific implementation varies, but the principle is consistent: separate key types for separate concerns.
Opaque Keys with Introspection
Both key types are opaque random strings — not JWTs. When a product service (like Smpl Config) receives a request with an SDK key, it doesn’t decode the key locally. It calls the app service’s introspection endpoint to validate the key and learn what it’s entitled to access.
We considered JWTs (self-validating tokens with embedded claims) and rejected them for three reasons:
Revocation. When you revoke an API key, it should stop working immediately. With JWTs, a revoked token continues working until it expires. You can mitigate this with short expiry times, but then you’re constantly reissuing tokens. Opaque keys with server-side validation can be revoked instantly.
Claim updates. When a customer changes their subscription tier, the key’s associated entitlements change. With JWTs, you’d need to reissue the token. With opaque keys, the introspection endpoint returns current entitlements on every call (or cache hit).
Information leakage. JWTs contain readable claims. An SDK key embedded in a client application would expose tenant IDs, scopes, and subscription details to anyone who base64-decodes it. Opaque keys reveal nothing.
Caching the Introspection Result
Calling the app service’s introspection endpoint on every request would add latency. So product services cache the result locally — in-process, per-node, with a 60-second TTL.
The cache is a simple TTL dictionary keyed by the SDK key value. On cache miss, the product service calls the introspection endpoint. On cache hit, it uses the cached entitlement data. Cache entries are a few hundred bytes of JSON, so even thousands of active keys consume negligible memory.
We considered Redis for a shared cache across nodes but rejected it. Redis adds a network hop (~0.5ms per lookup) to every request, plus infrastructure to operate. Local in-process caching provides sub-microsecond lookups with zero infrastructure.
The tradeoff is consistency: a revoked key may continue working for up to 60 seconds on nodes that have it cached. For smplkit’s use cases, this is acceptable. If instant revocation becomes a requirement, we can layer a push notification (SNS) to trigger immediate cache eviction without introducing Redis.
Database Representation
Both key types live in a single api_key table. A type column (API_KEY or SDK_KEY) determines the key’s behavior. The environment_id column is nullable — null for API keys (account-scoped), required for SDK keys (environment-scoped). Application-level validation enforces the constraint based on key type.
This single-table design keeps the data model simple and allows a single introspection query regardless of key type.
smplkit uses opaque API keys with server-side introspection for all authentication. Explore the API at docs.smplkit.com.