Skip to content

Library writeHydration can clobber another user's same-title row (missing userId in WHERE) #544

@electather

Description

@electather

Summary

writeHydration in apps/server/src/library/repo/hydrate.ts updates rows with WHERE eq(libraryItems.id, update.id) only — it does not scope by userId.

library_items has a composite primary key (user_id, id), where id = "<mediaType>:<tmdbId>" is unique only within a user (this is the multi-tenant fix from the library backend design — the same title can be owned by many users).

So when two users own the same title, one user's hydrate pass UPDATEs every user's row for that title, overwriting the others' sort_title, year, genres, servers, quality_tiers, watched_state, collection_*, and hydrated_at with the running user's values.

Impact

  • Cross-user data corruption of the denormalized browse projection.
  • One user's availability/quality chips, watched state, and sort key leak onto another user's row.
  • Silent — no error, the projection just shows wrong data for whichever user didn't hydrate last.

Where

writeHydration loops:

await db
  .update(libraryItems)
  .set({ /* sortTitle, year, genres, servers, qualityTiers, watchedState, collection*, hydratedAt */ })
  .where(eq(libraryItems.id, update.id)); // <-- missing userId

Fix

Scope the WHERE by the full composite key, mirroring the sibling writeProjection (added alongside the eager-projection fix), which already does this correctly:

.where(and(eq(libraryItems.userId, userId), eq(libraryItems.id, update.id)))

userId needs threading from the hydrate orchestrator into writeHydrationhydrate already has ctx.userId.

Tests

Add a multi-user regression test: two users own the same tmdbId, hydrate user A, assert user B's row is untouched.

Notes

Found 2026-06-03 while fixing the library first-paint bugs (eager metadata projection). The new writeProjection writer is correctly scoped; writeHydration was left as-is to stay surgical and flagged with an inline NOTE comment in repo/hydrate.ts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions