thedotmack/claude-mem · error

Forbidden

Forbidden

Error message

Invalid API key or insufficient scope

What it means

403 from the Postgres auth middleware: a key was presented but verifyPostgresApiKey returned null — unknown, revoked, or scope-deficient key. The route's requiredScopes are checked as part of verification, so a valid key lacking the needed scope (e.g. events:write for writeAuth routes) produces the same 403 as an invalid key.

Source

Thrown at src/server/middleware/postgres-auth.ts:100

      apiKeyId: null,
      mode: 'local-dev',
    };
    req.authContext = ctx;
    next();
    return;
  }

  if (!rawKey) {
    res.status(401).json({
      error: 'Unauthorized',
      message: 'Missing API key (Authorization: Bearer <key> or X-Api-Key: <key>)',
    });
    return;
  }

  const verified = await verifyPostgresApiKey(pool, rawKey, options.requiredScopes ?? []);
  if (!verified) {
    res.status(403).json({ error: 'Forbidden', message: 'Invalid API key or insufficient scope' });
    return;
  }

  const ctx: AuthContext = {
    userId: null,
    organizationId: null,
    teamId: verified.teamId,
    projectId: verified.projectId,
    scopes: verified.scopes,
    apiKeyId: verified.apiKeyId,
    mode: 'api-key',
  };
  req.authContext = ctx;
  next();
}

interface VerifiedPostgresApiKey {
  apiKeyId: string;

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Verify the key exists and is active in the Postgres instance the server points at.
  2. Compare the route's requiredScopes with the key's scopes; mint a new key including the missing scope via POST /v1/keys.
  3. Strip whitespace/newlines from the configured key value.
  4. Restart long-lived clients after key rotation so they reload credentials.

Example fix

# before: read-only key against a write route -> 403
curl -X POST https://host/v1/events -H 'X-Api-Key: ro-key' -d '{}'

# after: mint a key with the write scope
# POST /v1/keys { scopes: ['events:write', ...] } -> { key }
curl -X POST https://host/v1/events -H 'X-Api-Key: write-key' -d '{}'
Defensive patterns

Strategy: validation

Validate before calling

// Match route requirements to key scopes before sending
const ROUTE_SCOPES = { '/v1/events': ['events:write'] } as const;
function assertScopes(url: string, keyScopes: string[]) {
  const need = ROUTE_SCOPES[url];
  if (need && !need.every(s => keyScopes.includes(s))) {
    throw new Error(`Key lacks ${need.join(',')} for ${url}`);
  }
}

Type guard

function isInvalidKey403(res: Response): boolean { return res.status === 403; }

Prevention

When it happens

Trigger: POST /v1/events with a read-only key against writeAuth; a key minted in a different Postgres database (env mismatch); revoked or expired key still cached by the client; whitespace-corrupted key value.

Common situations: The MCP link flow mints read-only keys and a script reuses one for writes; staging vs production databases hold different keys; rotation happened but a long-running process kept the old key in memory.

Understand the failure class

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/35ef40e2e27d2a2f. Report an issue: GitHub.