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
- Verify the projectId matches a row in the projects table for the exact teamId being used
- Re-fetch the project list for the target team and use one of those IDs
- Check environment/config files for values taken from a different team's account
- 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
- Always resolve projectId from a team-scoped query, never from user input or cross-team cache
- Validate team/project pairing once at config load time
- Log both IDs on failure to spot cross-tenant mixing quickly
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
- server_session_id must belong to project_id and team_id
- Adapter rejected input
- agent_event source_id must belong to project_id and team_id
- API key is scoped to a different project
- Cannot bootstrap server API key…
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)