---
title: "@playstack/api-keys"
description: "Opaque customer API-key issuance, hashing, verification, scopes, usage, and revocation."
tags: ["package","identity","api-keys","security","free"]
---

{/* package-access:start */}

> **Free.** MIT licensed. Check preview availability before installing. See [package access](/docs/packages#access-policy).

{/* package-access:end */}

`@playstack/api-keys` authenticates customer and integration callers with opaque credentials that are shown once and stored only as indexed SHA-256 digests.

## Install

```sh
npm install @playstack/api-keys @playstack/core @playstack/events
```

## Compose and issue

```ts
import { createApiKeys } from '@playstack/api-keys'

export const apiKeys = createApiKeys({
  persistence,
  events,
  crypto: globalThis.crypto,
  clock,
  ids,
  product: 'bm',
  scopes: ['spans:read', 'spans:write'],
  maxKeyRateLimitCapacity: 1_000,
})

const { secret, key } = await apiKeys.issue({
  owner: { type: 'account', id: accountId },
  name: 'Production ingest',
  environment: 'live',
  scopes: ['spans:write'],
  createdBy: userId,
  rateLimitOverride: { capacity: 500 },
})
```

Return `secret` once. The stored record contains only its product/environment prefix, hash, and last four hexadecimal characters.

## Service configuration

| `ApiKeysOptions` property | Required | Purpose                                                                |
| ------------------------- | -------- | ---------------------------------------------------------------------- |
| `persistence`             | Yes      | Transactional lookup, locking, listing, and retained revocation state. |
| `events`                  | Yes      | Emits typed issue, use, and revocation events.                         |
| `crypto`                  | Yes      | Web Crypto `subtle` and `getRandomValues`.                             |
| `clock`, `ids`            | Yes      | Application-owned time and identifiers.                                |
| `product`                 | Yes      | Lowercase 2–16 character key prefix.                                   |
| `scopes`                  | Yes      | Complete allowlist of canonical product scopes.                        |
| `maxKeyRateLimitCapacity` | Yes      | Ceiling for any per-key capacity override.                             |
| `lastUsedWriteIntervalMs` | No       | Usage-write throttle; defaults to one hour.                            |
| `format` | No | Secret byte length and complete owner/environment prefix table. |

By default, secrets have the form `<product>_<live|test>_<32 hex characters>`
and contain 128 bits of CSPRNG entropy. For a product requiring 32-byte secrets,
set `format.secretBytes: 32`; supported integer lengths are 16–64 bytes.

```ts
// Supply this as ApiKeysOptions.format alongside the required ports.
const format = {
  secretBytes: 32,
  prefixes: {
    live: { user: 'pmk_u', account: 'pmk_a', accountMember: 'pmk_m' },
    test: { user: 'pmk_test_u', account: 'pmk_test_a', accountMember: 'pmk_test_m' },
  },
}
```

Prefixes omit the separator before the random suffix and use 1–64 lowercase
alphanumeric characters with single internal underscores, beginning with a letter.
`product` remains the canonical namespace, independent of the displayed prefix.
Issue and rotate share the snapshotted format; verification checks exact length,
prefix and stored owner/environment metadata. A prefix never grants authority.
Changing formats does not automatically accept older credentials: use explicit
`legacyVerifiers` only for application-owned imports, retaining live policy checks.

## Issue and verification properties

| Property                     | Meaning                                                                   |
| ---------------------------- | ------------------------------------------------------------------------- |
| `owner`                      | `{ type: account \| accountMember \| user, id }`.                         |
| `name`                       | Human-readable key label.                                                 |
| `environment`                | `live` or `test`, also encoded in the prefix.                             |
| `scopes`                     | Non-empty subset of the configured scope allowlist.                       |
| `createdBy`                  | Opaque actor ID.                                                          |
| `expiresAt`                  | Optional absolute millisecond timestamp.                                  |
| `rateLimitOverride.capacity` | Optional per-key ceiling that cannot exceed the service maximum.          |
| `requiredScopes`             | Optional verification requirement; every requested scope must be present. |

## Persistence bridge

`ApiKeysPersistence` exposes a transaction boundary, locked lookup by hash, lookup by ID, owner-scoped listing, and save. Verification and revocation must lock the same record so a last-used update cannot race past revocation.

```ts
import { prismaApiKeysPersistence } from '@playstack/api-keys/prisma'

const persistence = prismaApiKeysPersistence(prisma)
```

The managed Prisma fragment is security-critical and non-ejectable. The application owns migrations.

## Rate-limit and Nest composition

The key’s optional override is only an inner ceiling. Compose [`@playstack/rate-limit`](/docs/packages/events-operations/rate-limit) so an account-plan bucket remains authoritative, then add [`@playstack/nest-api-keys`](/docs/packages/identity/nest-api-keys) at the HTTP boundary to verify scopes and consume both buckets atomically.

Revocation retains the record and emits a transactional audit event. Manual rotation can still issue a second key, deploy it, verify traffic, and revoke the first.

## Rotate with a bounded grace period

```ts
const rotation = await apiKeys.rotate({
  id: currentKeyId,
  rotatedBy: userId,
  overlapEndsAt: Date.now() + 60 * 60_000,
})
```

Atomic rotation links both generations and keeps the previous key valid only through the configured overlap. `maxRotationOverlapMs` defaults to seven days. Retrying a completed rotation returns `already_rotated` rather than creating another key, but cannot recover the copy-once replacement secret.

`revokeFamily()` traverses both sides of the lineage and revokes every generation in one transaction. Missing links and cycles fail closed. Named legacy verifiers can validate an application-owned key format and optionally rehash the managed record during a gradual migration.

## Boundary

API keys are not JWTs, OAuth tokens, third-party credentials, offline licences, or a complete authorization policy. Application code still authorizes the verified owner and scopes against the requested resource.

## Live authorization policy

Optional `authorization.assertAllowed(input, transaction)` runs for issue, verify and rotate, including already-rotated retries. Verification checks policy on every use, not only last-used writes. Use the transaction to recheck membership, issuer ceilings and resource ownership; requested resource context is not trusted tenancy.

List/revoke management routes need separate authorization. Rotation preserves the original creator while recording the rotator; map replacement credentials to an application-owned stable grant actor. No generic machine-principal or vault-policy package is implied.

{/* package-reference:start */}

## API entry points and requirements

Reference snapshot: `@playstack/api-keys@0.1.0-beta.1`. Import only the entry point your runtime needs. Paths below are relative to the installed package; use **Go to Definition** in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.

| Public entry point | Declaration file |
| --- | --- |
| `@playstack/api-keys` | `./dist/index.d.ts` |
| `@playstack/api-keys/errors` | `./dist/errors.d.ts` |
| `@playstack/api-keys/prisma` | `./dist/prisma.d.ts` |
| `@playstack/api-keys/testing` | `./dist/testing.d.ts` |
| `@playstack/api-keys/playstack.artifacts.json` | No TypeScript declaration (asset or metadata export). |
| `@playstack/api-keys/package.json` | No TypeScript declaration (asset or metadata export). |

Node.js engine requirement: `>=20`. This is not a claim that every entry point works in browsers or Workers.

### Peer dependencies

This package declares no peer dependencies. Its ordinary dependencies are resolved by the package manager.

For a complete first program, start with [Getting started](/docs/getting-started). For API lookup and partial-example conventions, see [Reading the reference](/docs/packages#reading-the-reference). Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.

{/* package-reference:end */}
