Litestore · Developer guide

Keys

Learn how service API keys are minted, hashed and revoked, and what each of the six enforced scopes actually permits.

A ServiceApiKey authenticates a machine consumer — an integration, an AI agent, an outside assistant over MCP. It is the credential for a caller that has no session and no user behind it.

Keys are presented as a Bearer token and carry an lsk_ prefix, so a leaked key is recognisable as one in a log or a paste.

They are stored as a SHA-256 hash rather than as the key itself. The consequence is worth stating plainly: a key is readable exactly once, at creation. There is no recovery flow, because the database does not hold anything that could be recovered.

Scopes

Six scopes exist. Each one is enforced by a real request path.

That constraint is the whole design, and the constants file says why:

lib/api-keys/constants.ts
/** Available scopes for service API keys. Only scopes that are actually ENFORCED by a
 *  request path belong here — advertising a scope no consumer checks (formerly
 *  `write:catalog`/`write:users`/`admin:delete`) lets operators grant phantom power. */
export const API_SCOPES = [
  "read",
  "read:cart",
  "write:cart",
  "write:orders",
  "write:webhooks",
  "admin:mcp",
] as const

What each one permits:

  • read — read-only oRPC procedures, including listing and reading webhooks
  • read:cart — reading a cart through the cart endpoints
  • write:cart — creating and modifying a cart
  • write:orders — order writes through the service surface
  • write:webhooks — creating, updating, deleting and rotating webhook endpoints
  • admin:mcp — the operator MCP endpoint at app/api/mcp/admin

Why the list is short

Three scopes were removed: write:catalog, write:users and admin:delete.

They were removed because nothing checked them. A request carrying write:catalog was not treated differently from a request without it, anywhere in the codebase.

A scope that no code enforces is worse than no scope at all. An operator granting a key read plus write:catalog believes they have drawn a boundary, and they have drawn nothing — the phrase in the source comment is phantom power.

So the list is kept to what is real. A scope earns its place by having a request path that refuses without it.

Minting and revocation

Keys are minted from Settings → Developer, and creation is restricted to a super-admin.

Revocation is a flag rather than a deletion. isActive is re-read on every call rather than cached alongside the key, so deactivating a key takes effect on the next request.

That is the property that makes revocation useful under pressure. A cached authorization decision would leave a revoked key working until some session or cache expired; here there is no window to wait out.

An admin:mcp key carries the operator tool kit

Treat a key with admin:mcp like an admin credential: it reaches the admin's AI tool surface. Its read tools answer directly, and its change tools only ever mint approval cards an operator approves in the admin, so it cannot move money alone.

Rate limiting

Service endpoints are rate limited by rateLimits.publicApi on top of key authentication. Both MCP routes are covered.

Key authentication and rate limiting answer different questions. Authentication asks whether the caller is allowed; the limiter asks how often, and it applies to callers who are allowed.

The limiter fails open when Redis is unreachable. A Redis outage therefore degrades protection rather than blocking traffic — the store keeps serving requests it can no longer count.

That is a deliberate trade. The alternative, failing closed, would turn a cache outage into a full outage of the service surface.

On this page