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
- Verify the key exists and is active in the Postgres instance the server points at.
- Compare the route's requiredScopes with the key's scopes; mint a new key including the missing scope via POST /v1/keys.
- Strip whitespace/newlines from the configured key value.
- 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
- Mint keys with exactly the scopes each client's route list requires.
- Run a scope-matrix test in CI: every client route x key scope must pass.
- After rotation, restart long-lived processes to reload credentials.
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/35ef40e2e27d2a2f.
Report an issue: GitHub.