thedotmack/claude-mem · error
Forbidden
Forbidden
Error message
API key is not bound to a team
What it means
After validation passes, POST /api/sessions/observations requires an auth context bound to a team. If req.authContext.teamId is null, the adapter responds 403 with error 'Forbidden' and message 'API key is not bound to a team'. The API key used is valid but lacks team scoping.
Solutions
- Create/reissue the API key scoped to the intended team
- Verify the auth middleware attaches teamId to req.authContext for this key
- Check the Authorization key is the team-scoped one, not a global key
- If using project keys, ensure the project belongs to the team so teamId resolves
Example fix
// before: key without team scope Authorization: Bearer <global-key> // -> 403 Forbidden // after: team-scoped key Authorization: Bearer <team-scoped-key>
Defensive patterns
Strategy: validation
Validate before calling
if (!authContext?.teamId) {
throw new Error('API key is not team-scoped; /api/sessions/observations requires a team');
} Type guard
function isTeamScoped(ctx?: AuthContext): ctx is AuthContext & { teamId: string } {
return !!ctx?.teamId;
} Try / catch
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` }, ... });
if (res.status === 403) {
const body = await res.json();
throw new Error(`${body.error}: ${body.message}`);
} Prevention
- Provision API keys with team scope for compat endpoints
- Assert authContext.teamId exists in integration tests
- Avoid using global keys for tenant-scoped routes
- Verify key scopes after rotation
When it happens
Trigger: Calling the endpoint with an API key created without a team binding; auth middleware populated authContext but teamId was absent; using a global/admin key instead of a team-scoped key.
Common situations: Provisioning keys via scripts that skip team assignment; rotating to a new key with a different scope; using a service key in a multi-tenant deployment.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/3971bc3e34314d81.
Report an issue: GitHub.
Appendix: source
Thrown at src/server/compat/SessionsObservationsAdapter.ts:73
constructor(private readonly options: SessionsObservationsAdapterOptions) {}
setupRoutes(app: Application): void {
const writeAuth = requirePostgresServerAuth(this.options.pool, {
authMode: this.options.authMode,
allowLocalDevBypass: this.options.allowLocalDevBypass,
requiredScopes: ['memories:write'],
});
app.post('/api/sessions/observations', writeAuth, this.asyncHandler(async (req, res) => {
const parsed = observationsSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: 'ValidationError', issues: parsed.error.issues });
return;
}
const teamId = req.authContext?.teamId ?? null;
const projectId = req.authContext?.projectId ?? null;
if (!teamId) {
res.status(403).json({ error: 'Forbidden', message: 'API key is not bound to a team' });
return;
}
if (!projectId) {
// Compat mode requires a project-scoped key — the legacy payload does
// not carry a Server beta projectId, so without scope we cannot place
// the row in a tenant-scoped table.
res.status(400).json({
error: 'BadRequest',
message: 'Legacy /api/sessions/observations requires a project-scoped API key',
});
return;
}
try {
await this.ingestCompatObservation(req, res, parsed.data, teamId, projectId);
} catch (error) {
logger.error('SYSTEM', 'compat observations adapter failed', {
error: error instanceof Error ? error.message : String(error),View on GitHub (pinned to d8bc9755e7)