# Getting Started

This page walks through adding API key authentication to an API that currently has none. It is a practical starting point: a minimum viable implementation that you can extend with the patterns described in the rest of this guide.

## The Minimum Viable Key System

A working API key system needs four things:

1. A way to **generate** keys
2. A way to **store** key hashes
3. **Middleware** that validates keys on incoming requests
4. A way to **create and manage** keys (even if it starts as a script)

Everything else ([rotation](/docs/security/key-rotation), [rate limiting](/docs/security/rate-limiting), [expiration](/docs/security/expiration-policies), [leak detection](/docs/security/leak-detection)) is important for production but can be added incrementally. Start with the core loop: generate, store, validate.

## Step 1: Generate a Key

Use a cryptographically secure random source. Never use `Math.random()`, UUIDs, or timestamps.

```javascript
import { randomBytes } from "node:crypto";

function generateApiKey(prefix = "sk_live_") {
  const random = randomBytes(32).toString("base64url");
  return `${prefix}${random}`;
}

// Example output: sk_live_7Ks9xR2mP4wQzJ6vB8nY...
```

This gives you 256 bits of entropy encoded as a URL-safe base64 string, prefixed with a human-readable identifier. See [Key Generation](/docs/implementation/key-generation) for details on entropy requirements and encoding choices, and [Key Formats & Prefixes](/docs/implementation/key-formats-and-prefixes) for prefix conventions.

## Step 2: Store the Hash

> The database schema for these examples is in [Step 5](#step-5-set-up-the-database) below.

Never store the raw key. Store a SHA-256 hash. The raw key is shown to the developer once at creation and never stored on your side.

```javascript
import { createHash } from "node:crypto";

function hashKey(apiKey) {
  return createHash("sha256").update(apiKey).digest("hex");
}

// When creating a key:
const rawKey = generateApiKey();
const keyHash = hashKey(rawKey);

// Store keyHash in your database
await db.query(
  "INSERT INTO api_keys (key_hash, key_prefix, consumer_id, scopes, created_at) VALUES ($1, $2, $3, $4, NOW())",
  [keyHash, rawKey.slice(0, 8) /* "sk_live_" */, consumerId, ["read"]]
);

// Return rawKey to the developer -- this is the only time they see it
```

Index the `key_hash` column for fast lookups. See [Hashing & Storage](/docs/security/hashing-and-storage) for details on why SHA-256 is the right choice for API keys and when to consider salting.

## Step 3: Write the Validation Middleware

The middleware extracts the key from the request, hashes it, looks it up, and attaches the consumer identity to the request.

```javascript
import { createHash } from "node:crypto";

async function apiKeyAuth(req, res, next) {
  // Extract the key from the Authorization header
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing API key" });
  }
  const apiKey = authHeader.slice(7);

  // Hash and look up
  const keyHash = createHash("sha256").update(apiKey).digest("hex");
  const record = await db.query(
    "SELECT consumer_id, scopes FROM api_keys WHERE key_hash = $1 AND revoked_at IS NULL",
    [keyHash]
  );

  if (!record) {
    return res.status(401).json({ error: "Invalid API key" });
  }

  // Attach identity to the request
  req.consumer = record;
  next();
}

// Use it
app.use("/api", apiKeyAuth);
```

This is a simplified version of the [full validation middleware pattern](/docs/implementation/validation-and-lookup#validation-middleware), which covers timing-safe comparison, caching, and error handling in more detail.

**Important:** Return the same error response for "key not found" and "key revoked." Different responses for different failure modes let attackers enumerate valid keys. See [Consistent Error Messages](/docs/implementation/validation-and-lookup#important-consistent-error-messages).

## Step 4: Create a Key Management Endpoint

At minimum, you need a way to create keys and list them. This can start as an admin-only endpoint or even a CLI script.

```javascript
// POST /admin/api-keys — create a new key
app.post("/admin/api-keys", adminAuth, async (req, res) => {
  const { consumerId, scopes } = req.body;
  const rawKey = generateApiKey();
  const keyHash = hashKey(rawKey);

  await db.query(
    "INSERT INTO api_keys (key_hash, key_prefix, consumer_id, scopes, created_at) VALUES ($1, $2, $3, $4, NOW())",
    [keyHash, rawKey.slice(0, 8) /* "sk_live_" */, consumerId, scopes]
  );

  // Return the raw key — the only time it is visible
  res.json({
    key: rawKey,
    prefix: rawKey.slice(0, 8),
    consumer_id: consumerId,
    scopes,
  });
});

// GET /admin/api-keys — list keys (masked)
app.get("/admin/api-keys", adminAuth, async (req, res) => {
  const keys = await db.query(
    "SELECT key_prefix, consumer_id, scopes, created_at FROM api_keys WHERE revoked_at IS NULL"
  );
  res.json(keys);
});
```

As your needs grow, this evolves into a [self-service developer portal](/docs/operations/developer-portals) with rotation workflows, usage analytics, and role-based access control.

## Step 5: Set Up the Database

A minimal schema:

```sql
CREATE TABLE api_keys (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  key_hash TEXT NOT NULL UNIQUE,
  key_prefix TEXT NOT NULL,
  consumer_id TEXT NOT NULL,
  scopes TEXT[] NOT NULL DEFAULT '{}',
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  revoked_at TIMESTAMPTZ
);

CREATE INDEX idx_api_keys_hash ON api_keys (key_hash) WHERE revoked_at IS NULL;
```

The partial index (`WHERE revoked_at IS NULL`) ensures that lookups only scan active keys, which improves performance as your key count grows. See [Key Management at Scale](/docs/operations/key-management-at-scale) for a fuller schema with metadata, tags, and organization support.

## What to Add Next

Once the core loop works, extend it based on your needs:

| Concern | What to add | Guide |
| --- | --- | --- |
| Keys sitting in logs | Add a prefix-based masking filter to your logger | [Logging & Monitoring](/docs/operations/logging-and-monitoring) |
| No key expiration | Add an `expires_at` column and check it in middleware | [Expiration Policies](/docs/security/expiration-policies) |
| No rate limiting | Add per-key rate limits using a counter store | [Rate Limiting](/docs/security/rate-limiting) |
| Key rotation | Support multiple active keys per consumer with grace periods | [Key Rotation](/docs/security/key-rotation) |
| Scope enforcement | Check scopes against the endpoint's requirements in middleware | [Scoping & Permissions](/docs/security/scoping-and-permissions) |
| Leaked key response | Register your prefix with GitHub secret scanning | [Leak Detection](/docs/security/leak-detection) |
| Multiple services | Centralize validation in a gateway or shared library | [Multi-Service Auth](/docs/architecture/multi-service-authentication) |
| Scaling past DIY | Evaluate a managed gateway or platform | [Build vs. Buy](/docs/architecture/build-vs-buy) |

You do not need all of this on day one. A working key system with hashed storage, prefix-based identification, and consistent error handling is a solid foundation that can grow as your API matures.

## When to Skip the DIY Approach

If several of these apply to you, consider starting with a [managed gateway](/docs/architecture/gateway-based-authentication) or API key platform instead of building the minimum viable system yourself:

- You need to ship key auth within a week
- Your API will be consumed by external developers who expect a portal, documentation, and self-service key management
- You plan to [monetize the API](/docs/operations/api-monetization) with usage-based billing
- You do not have a dedicated backend or platform team to maintain the key system
- You are already running an API gateway for other reasons

There is no shame in skipping straight to a managed solution. The minimum viable implementation described on this page is valuable for understanding how key auth works under the hood, which helps you evaluate and operate any managed solution you adopt. But if your goal is a production API with key auth, the fastest path may be to use one.
