Skip to content

perf(user): add indexes on plexId, jellyfinUserId, and resetPasswordGuid#2806

Open
dougrathbone wants to merge 2 commits intoseerr-team:developfrom
dougrathbone:dougrathbone/perf/user-auth-indexes
Open

perf(user): add indexes on plexId, jellyfinUserId, and resetPasswordGuid#2806
dougrathbone wants to merge 2 commits intoseerr-team:developfrom
dougrathbone:dougrathbone/perf/user-auth-indexes

Conversation

@dougrathbone
Copy link
Copy Markdown
Contributor

@dougrathbone dougrathbone commented Apr 2, 2026

Description

User.plexId, User.jellyfinUserId, and User.resetPasswordGuid are looked up by value on every authentication request but have no database index. Every lookup is a full table scan.

While not a huge win on small instances, this seemed like a no-brainer quick win that might also help in situations where attackers try to brown out the host through brute force logins.

Query sites affected:

Column Code path Frequency
plexId Plex login (auth.ts), Tautulli watch stats, user import sync Every Plex login
jellyfinUserId Jellyfin login (auth.ts), avatar proxy (avatarproxy.ts), user sync Every Jellyfin/Emby login + every avatar load
resetPasswordGuid Password reset link validation (auth.ts) Every password reset

email is intentionally excluded -- its unique: true constraint already creates an implicit index in both SQLite and PostgreSQL.

Changes:

  • Added named @Index() decorators to the three columns in server/entity/User.ts
  • Added SQLite migration 1775000000000-AddUserLookupIndexes
  • Added PostgreSQL migration 1775000000000-AddUserLookupIndexes

Performance improvement:

Deployment size Before After
Small (< 50 users) Full table scan on every auth -- negligible Index seek -- negligible
Medium (100-500 users) Noticeable scan on busy instances Effectively instant
Large (500+ users, shared Plex/Jellyfin servers) Measurable latency on login and avatar requests Effectively instant

AI Disclosure: I used Claude Code to help write the tests and validate the SQLite db changes. I reviewed and understood all generated code before submitting.

How Has This Been Tested?

New test suite server/entity/User.index.test.ts using the real SQLite test database via setupTestDb().

Tests:

  1. user table has an index on plexId - queries sqlite_master and asserts IDX_user_plexId exists
  2. user table has an index on jellyfinUserId - same for IDX_user_jellyfinUserId
  3. user table has an index on resetPasswordGuid - same for IDX_user_resetPasswordGuid
  4. plexId lookup returns correct user - creates a user with a known plexId, queries by that value, asserts the correct record is returned

To run:

node server/test/index.mts server/entity/User.index.test.ts

To verify manually on a running instance:

SQLite:

SELECT name, tbl_name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'user';

PostgreSQL:

SELECT indexname FROM pg_indexes WHERE tablename = 'user';

Both should show IDX_user_plexId, IDX_user_jellyfinUserId, and IDX_user_resetPasswordGuid after migration.

Screenshots / Logs (if applicable)

N/A -- backend-only change with no UI impact.

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • I have updated the documentation accordingly. (no documentation changes required for this fix)
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract (no new UI strings added)
  • Database migration (if required)

@dougrathbone dougrathbone requested a review from a team as a code owner April 2, 2026 11:33
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Adds database indexes for three User lookup columns via entity decorators, PostgreSQL and SQLite migrations, and a test that verifies index creation and a plexId query against the test SQLite DB.

Changes

Cohort / File(s) Summary
User Entity Indexing
server/entity/User.ts
Added TypeORM @Index decorators with explicit names: IDX_user_plexId, IDX_user_jellyfinUserId, IDX_user_resetPasswordGuid.
Database Migrations
server/migration/postgres/1775000000000-AddUserLookupIndexes.ts, server/migration/sqlite/1775000000000-AddUserLookupIndexes.ts
New migrations creating the three indexes in up() and dropping them in down() for Postgres and SQLite respectively.
User Index Test Suite
server/entity/User.index.test.ts
New test initializing test DB, asserting the three sqlite_master indexes exist once, inserts a User (plexId: 99999) and verifies retrieval via repository query.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • danshilm

Poem

🐰 I dug three burrows in schema night,
Plex, Jellyfin, and a GUID light,
Migrations planted, tests took a spin,
Now lookups hop fast — a rabbit's grin! 🥕

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf(user): add indexes on plexId, jellyfinUserId, and resetPasswordGuid' directly and clearly summarizes the main change: adding database indexes to three User entity columns for performance optimization.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
server/migration/postgres/1775000000000-AddUserLookupIndexes.ts (1)

6-16: Consider IF NOT EXISTS for idempotency (same as SQLite migration).

PostgreSQL also supports IF NOT EXISTS for CREATE INDEX, which would make the migration idempotent in development environments where synchronize may have already created these indexes.

♻️ Proposed fix for idempotency
   public async up(queryRunner: QueryRunner): Promise<void> {
     await queryRunner.query(
-      `CREATE INDEX "IDX_user_plexId" ON "user" ("plexId")`
+      `CREATE INDEX IF NOT EXISTS "IDX_user_plexId" ON "user" ("plexId")`
     );
     await queryRunner.query(
-      `CREATE INDEX "IDX_user_jellyfinUserId" ON "user" ("jellyfinUserId")`
+      `CREATE INDEX IF NOT EXISTS "IDX_user_jellyfinUserId" ON "user" ("jellyfinUserId")`
     );
     await queryRunner.query(
-      `CREATE INDEX "IDX_user_resetPasswordGuid" ON "user" ("resetPasswordGuid")`
+      `CREATE INDEX IF NOT EXISTS "IDX_user_resetPasswordGuid" ON "user" ("resetPasswordGuid")`
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/migration/postgres/1775000000000-AddUserLookupIndexes.ts` around lines
6 - 16, The CREATE INDEX statements in the migration's up(queryRunner:
QueryRunner) are not idempotent; change each queryRunner.query call that creates
"IDX_user_plexId", "IDX_user_jellyfinUserId", and "IDX_user_resetPasswordGuid"
to use CREATE INDEX IF NOT EXISTS so the migration can be safely re-run in
environments where synchronize already created those indexes; keep the same
index names and SQL calls but add IF NOT EXISTS to each CREATE INDEX string.
server/migration/sqlite/1775000000000-AddUserLookupIndexes.ts (2)

6-16: Consider IF NOT EXISTS for idempotency in development environments.

The migration may fail in development if synchronize: true has already created these indexes from the entity decorators. Adding IF NOT EXISTS would make the migration idempotent.

♻️ Proposed fix for idempotency
   public async up(queryRunner: QueryRunner): Promise<void> {
     await queryRunner.query(
-      `CREATE INDEX "IDX_user_plexId" ON "user" ("plexId")`
+      `CREATE INDEX IF NOT EXISTS "IDX_user_plexId" ON "user" ("plexId")`
     );
     await queryRunner.query(
-      `CREATE INDEX "IDX_user_jellyfinUserId" ON "user" ("jellyfinUserId")`
+      `CREATE INDEX IF NOT EXISTS "IDX_user_jellyfinUserId" ON "user" ("jellyfinUserId")`
     );
     await queryRunner.query(
-      `CREATE INDEX "IDX_user_resetPasswordGuid" ON "user" ("resetPasswordGuid")`
+      `CREATE INDEX IF NOT EXISTS "IDX_user_resetPasswordGuid" ON "user" ("resetPasswordGuid")`
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/migration/sqlite/1775000000000-AddUserLookupIndexes.ts` around lines 6
- 16, The CREATE INDEX statements in the migration's up method (inside the
migration class implementing up(queryRunner: QueryRunner)) should be made
idempotent by adding IF NOT EXISTS to each SQL statement; update the three
queries for IDX_user_plexId, IDX_user_jellyfinUserId, and
IDX_user_resetPasswordGuid to use `CREATE INDEX IF NOT EXISTS ...` so the
migration won't fail when indexes already exist (e.g., when synchronize: true
created them).

18-22: Optional: Add IF EXISTS to down() for defensive rollback.

This is less critical than up(), but adding IF EXISTS prevents errors if the migration is rolled back after a partial failure.

♻️ Optional defensive fix
   public async down(queryRunner: QueryRunner): Promise<void> {
-    await queryRunner.query(`DROP INDEX "IDX_user_resetPasswordGuid"`);
-    await queryRunner.query(`DROP INDEX "IDX_user_jellyfinUserId"`);
-    await queryRunner.query(`DROP INDEX "IDX_user_plexId"`);
+    await queryRunner.query(`DROP INDEX IF EXISTS "IDX_user_resetPasswordGuid"`);
+    await queryRunner.query(`DROP INDEX IF EXISTS "IDX_user_jellyfinUserId"`);
+    await queryRunner.query(`DROP INDEX IF EXISTS "IDX_user_plexId"`);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/migration/sqlite/1775000000000-AddUserLookupIndexes.ts` around lines
18 - 22, Update the down(queryRunner: QueryRunner) method in the
AddUserLookupIndexes migration to use defensive DROP INDEX statements with IF
EXISTS; replace the three DROP INDEX "IDX_user_resetPasswordGuid",
"IDX_user_jellyfinUserId", and "IDX_user_plexId" calls so they execute DROP
INDEX IF EXISTS "IDX_user_resetPasswordGuid", DROP INDEX IF EXISTS
"IDX_user_jellyfinUserId", and DROP INDEX IF EXISTS "IDX_user_plexId"
respectively to avoid errors on partial rollbacks.
server/entity/User.index.test.ts (1)

49-52: Consider using assert.notStrictEqual for cleaner type narrowing.

The current pattern works but the ! assertion after assert.ok(found !== null) is slightly redundant. Using assert.notStrictEqual or assigning the assertion inline would be cleaner.

♻️ Optional refinement
     const found = await repo.findOne({ where: { plexId: 99999 } });
-    assert.ok(found !== null);
-    assert.strictEqual(found!.email, 'plextest@seerr.dev');
-    assert.strictEqual(found!.plexId, 99999);
+    assert.notStrictEqual(found, null);
+    assert.strictEqual(found?.email, 'plextest@seerr.dev');
+    assert.strictEqual(found?.plexId, 99999);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/entity/User.index.test.ts` around lines 49 - 52, Replace the
null-check pattern on the retrieved user to use assert.notStrictEqual for better
type narrowing: where the test calls repo.findOne(...) and then uses
assert.ok(found !== null) followed by non-null assertions on found (variable
found), change the assertion to assert.notStrictEqual(found, null) (or
assert.notStrictEqual(found, null, 'expected user to exist')) and then remove
the unnecessary non-null (!) operator when accessing found.email and
found.plexId so the TypeScript compiler correctly infers found is non-null.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/migration/postgres/1775000000000-AddUserLookupIndexes.ts`:
- Around line 6-22: The up() migration creates indexes without schema
qualification while down() drops them using "public" qualification, which can
fail on non-default schemas; update either up() or down() to be
consistent—preferably remove the "public" prefix from the DROP INDEX calls in
down() (referencing down() and the DROP INDEX statements for
"IDX_user_resetPasswordGuid", "IDX_user_jellyfinUserId", and "IDX_user_plexId")
or alternatively add "public" to the CREATE INDEX calls in up() so both up() and
down() use the same schema resolution when invoking queryRunner.query().

---

Nitpick comments:
In `@server/entity/User.index.test.ts`:
- Around line 49-52: Replace the null-check pattern on the retrieved user to use
assert.notStrictEqual for better type narrowing: where the test calls
repo.findOne(...) and then uses assert.ok(found !== null) followed by non-null
assertions on found (variable found), change the assertion to
assert.notStrictEqual(found, null) (or assert.notStrictEqual(found, null,
'expected user to exist')) and then remove the unnecessary non-null (!) operator
when accessing found.email and found.plexId so the TypeScript compiler correctly
infers found is non-null.

In `@server/migration/postgres/1775000000000-AddUserLookupIndexes.ts`:
- Around line 6-16: The CREATE INDEX statements in the migration's
up(queryRunner: QueryRunner) are not idempotent; change each queryRunner.query
call that creates "IDX_user_plexId", "IDX_user_jellyfinUserId", and
"IDX_user_resetPasswordGuid" to use CREATE INDEX IF NOT EXISTS so the migration
can be safely re-run in environments where synchronize already created those
indexes; keep the same index names and SQL calls but add IF NOT EXISTS to each
CREATE INDEX string.

In `@server/migration/sqlite/1775000000000-AddUserLookupIndexes.ts`:
- Around line 6-16: The CREATE INDEX statements in the migration's up method
(inside the migration class implementing up(queryRunner: QueryRunner)) should be
made idempotent by adding IF NOT EXISTS to each SQL statement; update the three
queries for IDX_user_plexId, IDX_user_jellyfinUserId, and
IDX_user_resetPasswordGuid to use `CREATE INDEX IF NOT EXISTS ...` so the
migration won't fail when indexes already exist (e.g., when synchronize: true
created them).
- Around line 18-22: Update the down(queryRunner: QueryRunner) method in the
AddUserLookupIndexes migration to use defensive DROP INDEX statements with IF
EXISTS; replace the three DROP INDEX "IDX_user_resetPasswordGuid",
"IDX_user_jellyfinUserId", and "IDX_user_plexId" calls so they execute DROP
INDEX IF EXISTS "IDX_user_resetPasswordGuid", DROP INDEX IF EXISTS
"IDX_user_jellyfinUserId", and DROP INDEX IF EXISTS "IDX_user_plexId"
respectively to avoid errors on partial rollbacks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4fa613c7-e9e9-4003-8072-f315583a276c

📥 Commits

Reviewing files that changed from the base of the PR and between 868430b and d650d48.

📒 Files selected for processing (4)
  • server/entity/User.index.test.ts
  • server/entity/User.ts
  • server/migration/postgres/1775000000000-AddUserLookupIndexes.ts
  • server/migration/sqlite/1775000000000-AddUserLookupIndexes.ts

Copy link
Copy Markdown
Collaborator

@fallenbagel fallenbagel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per our contributing guide, we expect AI-assisted development, not AI-driven development. This PR (along with #2804 and #2805) was submitted in rapid succession, and the quality issues across them suggest heavy AI generation with insufficient review.

Things I noticed in this PR:

  • Migrations appear to be AI-written and not generated. The sqlite and postgres files are byte-for-byte identical. This never happens with typeorm migration:generate. Please regenerate following the instructions in CONTRIBUTING.md.

  • Irrelevant test files written. You're testing that TypeORM applied an @Index decorator, which is TypeORM's job, not ours/applications. The tests are also sqlite-only and would break on postgres. This is also another red flag that led us to flag this PR. A human reviewer would have caught that these serve no purpose.

  • Why is an index being added on resetPasswordGuid? This is only hit during password resets (which doesn't happen always) and adds write overhead for negligible benefit, unlike plexId/jellyfinUserId which are used on every auth lookup.

CC: @seerr-team/seerr-core

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants