thedotmack/claude-mem · error · Error

project_id must belong to team_id

Error message

project_id must belong to team_id

What it means

assertProjectOwnership verifies that a given project ID actually belongs to the given team before allowing creates or API-key/audit-log writes. It runs a SELECT against the projects table filtering on both id and team_id; if no row matches, the relationship is invalid and the operation is refused. This prevents cross-team data injection through a valid but mis-scoped project_id.

Solutions

  1. Verify the projectId matches a row in the projects table for the exact teamId being used
  2. Re-fetch the project list for the target team and use one of those IDs
  3. Check environment/config files for values taken from a different team's account
  4. Confirm the project was not moved or deleted; recreate it under the correct team if needed

Example fix

// before
await create({ teamId: 'team_a', projectId: 'proj_of_team_b' });
// after
const projects = await listProjects('team_a');
await create({ teamId: 'team_a', projectId: projects[0].id });
Defensive patterns

Strategy: validation

Validate before calling

const row = await queryOne('SELECT id FROM projects WHERE id = $1 AND team_id = $2', [projectId, teamId]);
if (!row) throw new Error('project does not belong to team');

Try / catch

try { await createApiKey({ teamId, projectId }); }
catch (e) { if (e.message.includes('must belong to team_id')) { await refreshProjectIds(teamId); } else throw e; }

Prevention

When it happens

Trigger: Calling create, createApiKey, createAuditLog, or validateSource with a projectId that exists but is owned by a different team, or with a projectId that does not exist at all.

Common situations: Copy-pasting a project UUID from another team's dashboard; stale cached project IDs after a project was moved or deleted; multi-tenant setups where environment variables for project and team come from different sources.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at src/storage/postgres/utils.ts:62

  text: string,
  values: unknown[] = []
): Promise<T | null> {
  const result = await client.query<T>(text, values);
  return result.rows[0] ?? null;
}

export async function assertProjectOwnership(
  client: PostgresQueryable,
  projectId: string,
  teamId: string
): Promise<void> {
  const row = await queryOne<{ id: string }>(
    client,
    'SELECT id FROM projects WHERE id = $1 AND team_id = $2',
    [projectId, teamId]
  );
  if (!row) {
    throw new Error('project_id must belong to team_id');
  }
}

export async function assertSessionOwnership(
  client: PostgresQueryable,
  serverSessionId: string,
  projectId: string,
  teamId: string
): Promise<void> {
  const row = await queryOne<{ id: string }>(
    client,
    'SELECT id FROM server_sessions WHERE id = $1 AND project_id = $2 AND team_id = $3',
    [serverSessionId, projectId, teamId]
  );
  if (!row) {
    throw new Error('server_session_id must belong to project_id and team_id');
  }
}

View on GitHub (pinned to d8bc9755e7)