thedotmack/claude-mem · error

BadRequest

BadRequest

Error message

Legacy /api/sessions/observations requires a project-scoped API key

What it means

The legacy /api/sessions/observations compat route returns HTTP 400 BadRequest when the authenticated API key has no projectId in its auth context. The legacy Claude Code payload does not carry a Server beta projectId, so without a project-scoped key the adapter cannot place the observation row in a tenant-scoped table and rejects the request up front.

Solutions

  1. Create/use an API key scoped to a specific project instead of a team-wide key
  2. Verify the key provisioning flow sets projectId in the auth context (check verifyServerApiKey / key metadata)
  3. If this is a single-tenant deployment, bind the key to the default project
  4. Update client tooling or docs so new keys are always project-scoped

Example fix

// before
const key = createApiKey({ teamId: 'team_123' });
// after
const key = createApiKey({ teamId: 'team_123', projectId: 'proj_456' });
Defensive patterns

Strategy: validation

Validate before calling

// before calling the endpoint, assert the key is project-scoped
function assertProjectScopedKey(authContext) {
  if (!authContext?.projectId) throw new Error('API key must be project-scoped for legacy compat routes');
}
assertProjectScopedKey(authContext);

Type guard

const isProjectScoped = (ctx) => typeof ctx?.projectId === 'string' && ctx.projectId.length > 0;

Prevention

When it happens

Trigger: POST /api/sessions/observations with a valid API key that is team-scoped but not project-scoped (req.authContext.projectId is null).

Common situations: Operators minting legacy team-level API keys for older Claude Code clients after migrating to the multi-tenant Server beta; env/config where keys are provisioned without project binding; partial migration where the key rotation step was skipped.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/8ba4f2574b02e508. Report an issue: GitHub.

Appendix: source

Thrown at src/server/compat/SessionsObservationsAdapter.ts:80

    });

    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),
          contentSessionId: parsed.data.contentSessionId,
        });
        res.status(500).json({ stored: false, reason: 'internal_error' });
      }
    }));
  }

View on GitHub (pinned to d8bc9755e7)