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

  1. Create/reissue the API key scoped to the intended team
  2. Verify the auth middleware attaches teamId to req.authContext for this key
  3. Check the Authorization key is the team-scoped one, not a global key
  4. 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

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)