Skip to content

perf(database): index PersonalAccessToken.userId so token lookups stop seq-scanning - #4588

Merged
ericallam merged 1 commit into
mainfrom
feature/tri-13172-personalaccesstoken-lookups-by-userid-scan-tens-of-thousands
Aug 12, 2026
Merged

perf(database): index PersonalAccessToken.userId so token lookups stop seq-scanning#4588
ericallam merged 1 commit into
mainfrom
feature/tri-13172-personalaccesstoken-lookups-by-userid-scan-tens-of-thousands

Conversation

@ericallam

@ericallam ericallam commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

The two personal-access-token lookups by userId (one also filtering revokedAt is null, the other also filtering name) had no index on userId, so each did a full sequential scan of the PersonalAccessToken table to return a single row. userId is also an unindexed foreign key.

Fix

Add a single @@index([userId]). A user owns only a handful of PATs, so once userId is indexed each lookup touches a few rows and the residual revokedAt / name filter is trivial. Both query shapes lead with userId =, so one index serves both and a composite would only add write cost. The migration uses CREATE INDEX CONCURRENTLY IF NOT EXISTS, which is online-safe under write load and reversible by dropping the index.

Verified with a seeded local EXPLAIN: both queries go from a full sequential scan to an index scan on the new index.

@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d8145db

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds an index on PersonalAccessToken.userId in the Prisma schema and database migration. The migration creates PersonalAccessToken_userId_idx concurrently and only if it does not already exist. A server-change entry documents the owner-based lookup improvement.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the database performance change and the indexed field.
Description check ✅ Passed The description explains the problem, fix, deployment approach, and EXPLAIN verification, although it omits the repository template headings.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/tri-13172-personalaccesstoken-lookups-by-userid-scan-tens-of-thousands

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.

…scanning

The two personal-access-token lookups filtered by userId (one also
filtering revokedAt, the other also filtering name) had no index on
userId, so each seq-scanned the whole table to return a single row.
userId is also an unindexed foreign key. Add @@index([userId]); a user
owns few PATs, so the single-column index serves both query shapes
without needing a composite. Migration uses CREATE INDEX CONCURRENTLY
IF NOT EXISTS.
@ericallam
ericallam force-pushed the feature/tri-13172-personalaccesstoken-lookups-by-userid-scan-tens-of-thousands branch from b10055b to d8145db Compare August 12, 2026 12:37
@ericallam
ericallam marked this pull request as ready for review August 12, 2026 12:38

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9735e9ba-94fd-4795-9600-f91ce529ae4d

📥 Commits

Reviewing files that changed from the base of the PR and between 7b7d489 and b10055b.

📒 Files selected for processing (3)
  • .server-changes/index-personal-access-token-user-id.md
  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
  • internal-packages/database/prisma/schema.prisma
📜 Review details
⏰ Context from checks skipped due to timeout. (18)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 12)
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp
  • GitHub Check: typecheck / typecheck
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: code-quality / code-quality
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (2)
internal-packages/database/prisma/migrations/**/migration.sql

📄 CodeRabbit inference engine (internal-packages/database/CLAUDE.md)

internal-packages/database/prisma/migrations/**/migration.sql: When adding indexes to existing tables, use CREATE INDEX CONCURRENTLY IF NOT EXISTS to avoid production table locks.
Keep each CONCURRENTLY index in its own separate migration file, and add only one index per migration file.
Indexes on newly created tables may be created without CONCURRENTLY in the same migration as CREATE TABLE.
When adding an index for a new column on an existing table, use two migrations: first ALTER TABLE ... ADD COLUMN IF NOT EXISTS ..., then a separate migration containing CREATE INDEX CONCURRENTLY IF NOT EXISTS ....

Files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
internal-packages/database/prisma/schema.prisma

📄 CodeRabbit inference engine (internal-packages/database/CLAUDE.md)

internal-packages/database/prisma/schema.prisma: Define the database schema in prisma/schema.prisma using Prisma 6.14.0 for PostgreSQL.
New code must always target RunEngineVersion.V2; V1 is retired and retained only for historical rows and rejection.

Files:

  • internal-packages/database/prisma/schema.prisma
🧠 Learnings (19)
📓 Common learnings
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: internal-packages/database/prisma/migrations/20260318114244_add_prompt_friendly_id/migration.sql:5-5
Timestamp: 2026-03-22T13:49:23.474Z
Learning: In `internal-packages/database/prisma/migrations/**/*.sql`: When a column and its index are added in a follow-up migration file but the parent table itself was introduced in the same PR (i.e., no production rows exist yet), a plain `CREATE INDEX` / `CREATE UNIQUE INDEX` (without CONCURRENTLY) is safe and does not require splitting into a separate migration. The CONCURRENTLY requirement only applies when the table already has existing data in production.
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 4148
File: apps/webapp/app/models/orgMember.server.ts:149-168
Timestamp: 2026-07-03T17:10:32.244Z
Learning: In triggerdotdev/trigger.dev, `User.email` (schema in `internal-packages/database/prisma/schema.prisma`) has only a case-sensitive unique btree index — there is no `citext` type or `lower(email)` functional index. Using Prisma's `mode: "insensitive"` on `User.email` lookups therefore forces a sequential scan on the users table, which is a real performance regression risk under load (e.g., WorkOS Directory Sync backfill bursts). Most write paths already lowercase email before writing (e.g., magic-link login in `apps/webapp/app/routes/login.magic/route.tsx`, Google OAuth), so mixed-case rows are narrowly possible only via providers like GitHub OAuth that may return non-lowercased addresses. The correct long-term fix is normalizing email on all write paths and/or adding a `citext`/functional-unique-index migration with a backfill, tracked as a separate app-wide change rather than bolted onto individual feature PRs (e.g., `apps/webapp/app/models/orgMember.server.ts`'s `ensureUserForDirecto...
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Applies to internal-packages/database/prisma/migrations/**/migration.sql : When adding an index for a new column on an existing table, use two migrations: first `ALTER TABLE ... ADD COLUMN IF NOT EXISTS ...`, then a separate migration containing `CREATE INDEX CONCURRENTLY IF NOT EXISTS ...`.
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: internal-packages/database/prisma/migrations/20260129162810_add_integration_deployment/migration.sql:14-18
Timestamp: 2026-02-03T18:48:39.285Z
Learning: When adding indexes to existing tables in Prisma migrations, use CONCURRENTLY in a separate migration file to avoid table locks. Indexes on newly created tables (CREATE TABLE) can be created in the same migration file without CONCURRENTLY.
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Applies to internal-packages/database/prisma/migrations/**/migration.sql : When adding indexes to existing tables, use `CREATE INDEX CONCURRENTLY IF NOT EXISTS` to avoid production table locks.
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3368
File: internal-packages/database/prisma/schema.prisma:666-666
Timestamp: 2026-04-16T14:21:18.496Z
Learning: In `triggerdotdev/trigger.dev`, the `BackgroundWorkerTask` covering index on `(runtimeEnvironmentId, slug, triggerSource)` lives in `internal-packages/database/prisma/migrations/20260413000000_add_bwt_covering_index/migration.sql` as a `CREATE INDEX CONCURRENTLY IF NOT EXISTS`, intentionally in its own migration file separate from the `TaskIdentifier` table migration. Do not flag this index as missing from the schema migrations in future reviews.
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Applies to internal-packages/database/prisma/migrations/**/migration.sql : Keep each `CONCURRENTLY` index in its own separate migration file, and add only one index per migration file.
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Applies to internal-packages/database/prisma/migrations/**/migration.sql : Indexes on newly created tables may be created without `CONCURRENTLY` in the same migration as `CREATE TABLE`.
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Pre-apply newly added indexes manually in production before deploying the migration; Prisma should skip creation when the index already exists.
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Applies to internal-packages/database/prisma/schema.prisma : Define the database schema in `prisma/schema.prisma` using Prisma 6.14.0 for PostgreSQL.
📚 Learning: 2026-07-26T13:14:02.968Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 4378
File: .server-changes/realtime-run-reads-from-primary.md:0-0
Timestamp: 2026-07-26T13:14:02.968Z
Learning: For files in the .server-changes directory, the body text is published verbatim as dashboard-facing user release notes. Write entries in terms of user-visible behavior (what users can do/see), and avoid implementation-oriented details such as environment-variable names, internal mechanisms, or configuration knobs. If you need to include operational/configuration specifics, put those details in the PR description instead of the .server-changes entry.

Applied to files:

  • .server-changes/index-personal-access-token-user-id.md
📚 Learning: 2026-07-03T17:10:32.244Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 4148
File: apps/webapp/app/models/orgMember.server.ts:149-168
Timestamp: 2026-07-03T17:10:32.244Z
Learning: In triggerdotdev/trigger.dev, `User.email` (schema in `internal-packages/database/prisma/schema.prisma`) has only a case-sensitive unique btree index — there is no `citext` type or `lower(email)` functional index. Using Prisma's `mode: "insensitive"` on `User.email` lookups therefore forces a sequential scan on the users table, which is a real performance regression risk under load (e.g., WorkOS Directory Sync backfill bursts). Most write paths already lowercase email before writing (e.g., magic-link login in `apps/webapp/app/routes/login.magic/route.tsx`, Google OAuth), so mixed-case rows are narrowly possible only via providers like GitHub OAuth that may return non-lowercased addresses. The correct long-term fix is normalizing email on all write paths and/or adding a `citext`/functional-unique-index migration with a backfill, tracked as a separate app-wide change rather than bolted onto individual feature PRs (e.g., `apps/webapp/app/models/orgMember.server.ts`'s `ensureUserForDirecto...

Applied to files:

  • .server-changes/index-personal-access-token-user-id.md
📚 Learning: 2026-05-14T14:54:39.095Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3545
File: .server-changes/agent-view-sessions.md:10-10
Timestamp: 2026-05-14T14:54:39.095Z
Learning: In the `trigger.dev` repository, do not flag inconsistent dot vs slash notation in route/path strings inside `.server-changes/*.md` files. These markdown files are consumed verbatim into the changelog, so the mixed notation (e.g., `resources.orgs.../runs.$runParam/...`) is intentional and should be preserved as-is.

Applied to files:

  • .server-changes/index-personal-access-token-user-id.md
📚 Learning: 2026-07-13T14:51:27.502Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Applies to internal-packages/database/prisma/migrations/**/migration.sql : Indexes on newly created tables may be created without `CONCURRENTLY` in the same migration as `CREATE TABLE`.

Applied to files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2026-03-22T13:49:20.068Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: internal-packages/database/prisma/migrations/20260318114244_add_prompt_friendly_id/migration.sql:5-5
Timestamp: 2026-03-22T13:49:20.068Z
Learning: For Prisma migration SQL files under `internal-packages/database/prisma/migrations/`, it is acceptable to create indexes with `CREATE INDEX` / `CREATE UNIQUE INDEX` (i.e., without `CONCURRENTLY`) when the parent table is introduced in the same PR and has no existing production rows yet. Only require `CREATE INDEX CONCURRENTLY` (or otherwise account for existing production data/locks) when the table already exists in production with data.

Applied to files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
📚 Learning: 2026-07-13T14:51:27.502Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Applies to internal-packages/database/prisma/migrations/**/migration.sql : Keep each `CONCURRENTLY` index in its own separate migration file, and add only one index per migration file.

Applied to files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2026-07-13T14:51:27.502Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Applies to internal-packages/database/prisma/migrations/**/migration.sql : When adding an index for a new column on an existing table, use two migrations: first `ALTER TABLE ... ADD COLUMN IF NOT EXISTS ...`, then a separate migration containing `CREATE INDEX CONCURRENTLY IF NOT EXISTS ...`.

Applied to files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2026-07-13T14:51:27.502Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Applies to internal-packages/database/prisma/migrations/**/migration.sql : When adding indexes to existing tables, use `CREATE INDEX CONCURRENTLY IF NOT EXISTS` to avoid production table locks.

Applied to files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2026-02-03T18:48:31.790Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: internal-packages/database/prisma/migrations/20260129162810_add_integration_deployment/migration.sql:14-18
Timestamp: 2026-02-03T18:48:31.790Z
Learning: For Prisma migrations targeting PostgreSQL: - When adding indexes to existing tables, create the index in a separate migration file and include CONCURRENTLY to avoid locking the table. - For indexes on newly created tables (in CREATE TABLE statements), you can create the index in the same migration file without CONCURRENTLY. This reduces rollout complexity for new objects while protecting uptime for existing structures.

Applied to files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
📚 Learning: 2026-04-16T14:21:18.496Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3368
File: internal-packages/database/prisma/schema.prisma:666-666
Timestamp: 2026-04-16T14:21:18.496Z
Learning: In `triggerdotdev/trigger.dev`, the `BackgroundWorkerTask` covering index on `(runtimeEnvironmentId, slug, triggerSource)` lives in `internal-packages/database/prisma/migrations/20260413000000_add_bwt_covering_index/migration.sql` as a `CREATE INDEX CONCURRENTLY IF NOT EXISTS`, intentionally in its own migration file separate from the `TaskIdentifier` table migration. Do not flag this index as missing from the schema migrations in future reviews.

Applied to files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2026-07-13T14:51:27.502Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Pre-apply newly added indexes manually in production before deploying the migration; Prisma should skip creation when the index already exists.

Applied to files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2026-07-13T14:51:27.502Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Applies to internal-packages/database/prisma/schema.prisma : Define the database schema in `prisma/schema.prisma` using Prisma 6.14.0 for PostgreSQL.

Applied to files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2026-05-12T21:06:01.771Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3542
File: internal-packages/database/prisma/schema.prisma:761-769
Timestamp: 2026-05-12T21:06:01.771Z
Learning: In the triggerdotdev/trigger.dev repository, the `PlaygroundConversation` model (internal-packages/database/prisma/schema.prisma) intentionally stores `userId` as a plain `String` (no FK relation to `User`). This is a deliberate architectural decision: the model is designed as a generic durable-stream primitive where `userId` is one of several customer-supplied free-form identifiers (alongside `externalId`, tags, JSON `metadata`). Cascade/orphan handling and cross-tenant integrity are enforced at the application/query layer, not via Postgres FK constraints. Do not suggest adding a User relation or FK for this field.

Applied to files:

  • internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql
📚 Learning: 2026-03-22T13:49:23.474Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: internal-packages/database/prisma/migrations/20260318114244_add_prompt_friendly_id/migration.sql:5-5
Timestamp: 2026-03-22T13:49:23.474Z
Learning: In `internal-packages/database/prisma/migrations/**/*.sql`: When a column and its index are added in a follow-up migration file but the parent table itself was introduced in the same PR (i.e., no production rows exist yet), a plain `CREATE INDEX` / `CREATE UNIQUE INDEX` (without CONCURRENTLY) is safe and does not require splitting into a separate migration. The CONCURRENTLY requirement only applies when the table already has existing data in production.

Applied to files:

  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to internal-packages/database/**/*.{ts,tsx} : Use Prisma for database interactions in internal-packages/database with PostgreSQL

Applied to files:

  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2026-07-13T14:51:27.502Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-07-13T14:51:27.502Z
Learning: Create migrations by editing `prisma/schema.prisma`, running `pnpm run db:migrate:dev:create --name "descriptive_name"` from `internal-packages/database`, cleaning up the listed extraneous generated migration objects and indexes, then running `pnpm run db:migrate:deploy && pnpm run generate`.

Applied to files:

  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2025-08-14T10:35:38.344Z
Learnt from: myftija
Repo: triggerdotdev/trigger.dev PR: 2391
File: packages/cli-v3/src/commands/login.ts:99-0
Timestamp: 2025-08-14T10:35:38.344Z
Learning: In the whoami v2 endpoint, organization access tokens (OATs) return the same schema structure as personal access tokens (PATs), so existing CLI flows that expect userId and email fields work correctly with both token types.

Applied to files:

  • internal-packages/database/prisma/schema.prisma
📚 Learning: 2026-04-30T21:28:35.705Z
Learnt from: samejr
Repo: triggerdotdev/trigger.dev PR: 3473
File: internal-packages/database/prisma/schema.prisma:59-60
Timestamp: 2026-04-30T21:28:35.705Z
Learning: When reviewing Prisma schema files in this repository, do not suggest using Prisma’s `@check` model/table-level attribute or any native Prisma schema syntax for CHECK constraints. Prisma does not implement CHECK constraints (see prisma/prisma#3388). If a CHECK constraint is required, add it only via raw SQL in a handwritten migration (e.g., `ALTER TABLE ... ADD CONSTRAINT ... CHECK (...)`).

Applied to files:

  • internal-packages/database/prisma/schema.prisma
🔇 Additional comments (2)
internal-packages/database/prisma/schema.prisma (1)

158-159: LGTM!

internal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sql (1)

1-1: 🩺 Stability & Availability

Test the Prisma 6.14.0 migration path.

Run prisma migrate deploy against a disposable PostgreSQL database. CREATE INDEX CONCURRENTLY must execute outside a transaction. Keep this statement as the only statement in this migration.

Comment thread .server-changes/index-personal-access-token-user-id.md
@ericallam
ericallam merged commit 96b2959 into main Aug 12, 2026
42 checks passed
@ericallam
ericallam deleted the feature/tri-13172-personalaccesstoken-lookups-by-userid-scan-tens-of-thousands branch August 12, 2026 13:04
@github-actions github-actions Bot mentioned this pull request Aug 12, 2026
ericallam pushed a commit that referenced this pull request Aug 13, 2026
## Summary
4 new features, 24 improvements, 10 bug fixes.

## Highlights

- Allow `trigger deploy` to authenticate with an environment API key
from `TRIGGER_ACCESS_TOKEN`.
([#4561](#4561))

## Improvements
- Chat in the browser now reconnects when the connection drops mid-turn,
instead of leaving the reply stuck as if it were still generating.
Reports can be fetched as structured data with the `json` format, and
the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`).
The `mint-token` command's help is clearer too: a token minted without
`--cap` is read-only, and `--ttl` shows the correct maximum lifetime of
7 days.
([#4418](#4418))
- The dev environment onboarding now tracks real progress. After you run
`init`, the setup checklist marks your project as initialized, and it
updates live as your dev server connects and your tasks register. The
blank state also adds a "Copy AI agent prompt" button that copies a
ready-to-paste setup prompt (pre-filled with your project reference) for
Claude Code, Cursor, or any coding agent.
([#4563](#4563))
  
The `init` scaffold now imports from `@trigger.dev/sdk` instead of the
deprecated `@trigger.dev/sdk/v3` subpath.
- Deployed images now ship dependencies and bundled task code as
separate layers. Repeat deploys with unchanged dependencies typically
push and pull far less data, making deploys and worker image pulls
faster.
([#4551](#4551))
- The current-worker API now reports each task's queue, so you can see
which tasks write to a given queue.
([#4525](#4525))
- Watch-mode chat streams now survive quiet windows and page reloads,
and a reply cut off by a lost connection shows an error instead of
appearing finished. Aborting a resumed subscription only closes your
local stream — call `stopGeneration(chatId)` or pass `stopOnAbort: true`
to stop the run. Also fixed a race where quickly restarting a stream
could break stop and reconnect, and stopping a chat now hands it back to
your other tabs instead of leaving them read-only.
([#4516](#4516))

## Server changes

These changes affect the self-hosted Docker image and Trigger.dev Cloud:

- The dashboard agent now has a monthly message allowance and plan-based
limits on watches. Queries stay read-only with clearer errors when busy,
and messages with unusual characters no longer fail to send.
([#4516](#4516))
- Meet the dashboard agent: a chat in every environment that answers
questions about your runs, queues, errors and health with real data and
links, replacing Ask AI everywhere it used to appear. Investigate a
failed run, an error, a backed-up queue or a run that hasn't started to
get a worked-through answer — what happened, why, and how to fix it,
with every claim linked to the runs, errors and deploys behind it. It
reads your data read-only, works on preview and dev branches with that
branch's own data, and reads the same everywhere — dashboard, terminal,
editor. A very long chat keeps working: the agent summarises the earlier
part and carries on.
  
**Watch…** on a run, queue, error or the health report tells you when
things change: a run finishes, a queue clears or grows past a number you
pick, an error comes back, an environment recovers. The answer arrives
in the chat and, if you want, by email, Slack or webhook — and the agent
can look into bad news on its own. A watch reaches you on any browser
you sign in from, without opening the chat first.
  
A sample of conversations is scored automatically so the agent keeps
getting better; only the score and a one-line summary are kept, never
your messages, data or code, and we can switch it off for your
organization on request. Ask the agent instead of the Docs buttons in
page headers — they stay there when the agent isn't available to you.
Separately, a queue's wait times, peak depth, throughput and throttling
can now be read from the API.
([#4418](#4418))
- Add backend support for delaying cron schedules within a specified
window with a minimum of 60 seconds.
([#4566](#4566))
- Reduced recurring background database load from the billing-limit
recovery check, so paused environments are reconciled with less
overhead.
([#4590](#4590))
- Validating a schedule when deploying or updating a schedule now does
less work on projects with many preview branches, so those operations
stay fast as branches accumulate.
([#4598](#4598))
- Project pages now load faster for projects with a large number of
preview branches, by no longer loading archived branch environments that
aren't shown.
([#4595](#4595))
- Database queries that filter on a list of values now reuse cached
query plans more consistently, instead of forcing the database to
re-plan whenever the list length changes.
([#4480](#4480))
- Routine cleanup of old dashboard agent data now runs on its own
schedule.
([#4599](#4599))
- Database connection metrics are now reported for every configured
database connection instead of only the primary one, and stay accurate
regardless of connection type.
([#4541](#4541))
- Deployment-related API endpoints now draw from their own generous rate
limit budget, configurable via the `DEPLOYMENT_RATE_LIMIT_*` environment
variables, so runtime API traffic no longer competes with deployments
for the same per-environment budget.
([#4565](#4565))
- Deleting or editing a secret environment variable is now fast and no
longer slows down as a project accumulates variables.
([#4555](#4555))
- Speed up personal access token lookups by indexing them on their owner
([#4588](#4588))
- Switching project or organization in the sidebar now keeps you on the
same page instead of sending you back to Tasks. Pages for a specific
run, deploy or other single item open the matching list instead.
([#4585](#4585))
- Reduced database load when loading the dashboard by removing an unused
organization member count that was being calculated on every page
navigation.
([#4587](#4587))
- The environment variables page now loads a page at a time, keeping it
fast for projects with a large number of variables. Search matches
variable names across every page.
([#4597](#4597))
- Groundwork for an alternative database connection driver, gated behind
configuration and disabled by default, so there is no change to default
behavior.
([#4539](#4539))
- Deleting an alert channel is now fast and no longer slows down as a
project builds up alert history.
([#4554](#4554))
- Reduced internal overhead on the API under high load.
([#4532](#4532))
- Out-of-date upgrade prompts no longer appear in the dashboard: the
"V4" badges and the notices saying preview branches and the queues table
need V4 have been removed. The side menu still warns you when a project
is on v3, with updated wording and a link to the v4 upgrade guide.
([#4589](#4589))
- Make background worker registration cheaper for projects with many
scheduled tasks by scoping declarative schedule reconciliation to the
current environment and dropping redundant schedule lookups.
([#4577](#4577))
- Speed up setting and importing environment variables for projects with
many variables.
([#4579](#4579))
- Loading the deployments list is now faster, especially when filtering
by deployment status on projects with many deployments.
([#4591](#4591))
- Fixed the billing limits page timing out for organizations with many
preview branches, especially while a spend limit was being enforced. The
page now loads quickly, so you can raise or resolve your limit without
delay. ([#4594](#4594))
- Fix the Concurrency page showing the plan's default concurrency for
the dev environment instead of the environment's actual limit.
([#4596](#4596))
- Creating an organization sometimes left you back on the creation form
even though the organization had already been created, so clicking
Create again made a duplicate. Creating an organization now completes
and takes you to your new organization.
([#4530](#4530))
- Ensure creating a project completes instead of returning to its
creation form after a navigation error.
([#4584](#4584))
- Renaming a project now keeps you on the project settings page and
tells you what happened, instead of silently moving you to the tasks
page or clearing the form with no explanation.
([#4601](#4601))
- Fixed support threads showing no account details for some customers,
so the team can see your plan, organizations and projects when you get
in touch.
([#4575](#4575))
- In the light theme, the Format, Clear and Copy buttons on the query
editor no longer blend into the query text behind them.
([#4592](#4592))
- The health report now says start latency is "unknown" when there is no
data for it, instead of showing a healthy-looking 0ms
([#4544](#4544))
- Realtime streams written inside a chat session run now use the same
backend as the session itself, and runs are no longer created against a
backend that cannot serve them.
([#4564](#4564))
- The grouped "watch updates" notification now shows the total number of
results waiting, instead of only the most recent batch's count.
([#4525](#4525))

<details>
<summary>Raw changeset output</summary>

# Releases
## @trigger.dev/build@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
## trigger.dev@4.5.11

### Patch Changes

- Chat in the browser now reconnects when the connection drops mid-turn,
instead of leaving the reply stuck as if it were still generating.
Reports can be fetched as structured data with the `json` format, and
the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`).
The `mint-token` command's help is clearer too: a token minted without
`--cap` is read-only, and `--ttl` shows the correct maximum lifetime of
7 days.
([#4418](#4418))
- Allow `trigger deploy` to authenticate with an environment API key
from `TRIGGER_ACCESS_TOKEN`.
([#4561](#4561))
- The dev environment onboarding now tracks real progress. After you run
`init`, the setup checklist marks your project as initialized, and it
updates live as your dev server connects and your tasks register. The
blank state also adds a "Copy AI agent prompt" button that copies a
ready-to-paste setup prompt (pre-filled with your project reference) for
Claude Code, Cursor, or any coding agent.
([#4563](#4563))

The `init` scaffold now imports from `@trigger.dev/sdk` instead of the
deprecated `@trigger.dev/sdk/v3` subpath.

- Deployed images now ship dependencies and bundled task code as
separate layers. Repeat deploys with unchanged dependencies typically
push and pull far less data, making deploys and worker image pulls
faster.
([#4551](#4551))
- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
  - `@trigger.dev/build@4.5.11`
  - `@trigger.dev/schema-to-json@4.5.11`
## @trigger.dev/core@4.5.11

### Patch Changes

- Chat in the browser now reconnects when the connection drops mid-turn,
instead of leaving the reply stuck as if it were still generating.
Reports can be fetched as structured data with the `json` format, and
the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`).
The `mint-token` command's help is clearer too: a token minted without
`--cap` is read-only, and `--ttl` shows the correct maximum lifetime of
7 days.
([#4418](#4418))
- The current-worker API now reports each task's queue, so you can see
which tasks write to a given queue.
([#4525](#4525))
## @trigger.dev/python@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
  - `@trigger.dev/sdk@4.5.11`
  - `@trigger.dev/build@4.5.11`
## @trigger.dev/react-hooks@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
## @trigger.dev/redis-worker@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
## @trigger.dev/rsc@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
## @trigger.dev/schema-to-json@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
## @trigger.dev/sdk@4.5.11

### Patch Changes

- Chat in the browser now reconnects when the connection drops mid-turn,
instead of leaving the reply stuck as if it were still generating.
Reports can be fetched as structured data with the `json` format, and
the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`).
The `mint-token` command's help is clearer too: a token minted without
`--cap` is read-only, and `--ttl` shows the correct maximum lifetime of
7 days.
([#4418](#4418))
- Watch-mode chat streams now survive quiet windows and page reloads,
and a reply cut off by a lost connection shows an error instead of
appearing finished. Aborting a resumed subscription only closes your
local stream — call `stopGeneration(chatId)` or pass `stopOnAbort: true`
to stop the run. Also fixed a race where quickly restarting a stream
could break stop and reconnect, and stopping a chat now hands it back to
your other tabs instead of leaving them read-only.
([#4516](#4516))
- Updated dependencies:
  - `@trigger.dev/core@4.5.11`

</details>

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ericallam added a commit that referenced this pull request Aug 14, 2026
## What

A relation with `onDelete: Cascade | SetNull` whose child FK column has
no index makes every parent delete fire a cascade that sequentially
scans the whole child table. That has shipped three times recently and
had to be fixed after the fact (#4554 `ProjectAlert.channelId`, #4555
`EnvironmentVariableValue.valueReferenceId`, #4588
`PersonalAccessToken.userId`).

This adds a schema-aware CI guard that catches the next one before it
merges.

## How

`apps/webapp/scripts/fkCascadeIndexGuard.ts` parses both Prisma schemas
(`@trigger.dev/database`, `@internal/run-ops-database`) and flags any
`onDelete: Cascade | SetNull` relation whose leading FK scalar is not
the leading column of some index (`@@index` / `@@unique` / `@@id` /
field-level `@id`/`@unique`) on the child model. A leading FK column
lets the cascade's `WHERE fk = $1` use the index instead of a seq scan.

It is modeled on the existing `runOpsLegacyGuard` (same `--check` gate,
same baseline-regenerate pattern), and it is lighter: it only reads
`schema.prisma` as text, so its CI job needs no Prisma client generation
and no raised heap.

## Why a baseline, not a hard rule

Not every unindexed cascade FK is a live bug. When the parent is only
ever soft-deleted, the cascade never fires, so the missing index is
harmless. Hard vs soft delete lives in application code
(`parent.delete()` vs `parent.update({ deletedAt })`), not in the
schema, and a `deletedAt` column proves neither direction. So the guard
makes no such judgment: it flags every unindexed cascade FK uniformly
and carries a baseline of the 72 currently-accepted cases. Only
violations **not** in the baseline fail `--check`.

The value is the forcing function: a newly added cascade FK stops CI and
makes the author answer "is the parent ever hard-deleted?" Add the index
if yes; regenerate the baseline with a reason if no.

## Wiring

- `apps/webapp/package.json`: `guard:fk-cascade-index` script
(regenerate with no args, gate with `-- --check`).
- `.github/workflows/fk-cascade-guard.yml`: the reusable workflow.
- `.github/workflows/pr_checks.yml`: runs on webapp-affecting changes,
aggregated into `all-checks`.

## Verification

- The three already-fixed columns are correctly seen as indexed (absent
from the baseline).
- `--check` passes on the current schemas (72 baselined, 0 new).
- A synthetic new unindexed cascade FK fails with exit 1 and an
actionable message.
- Adding `@@index([fk])`, or a composite leading with the FK, clears it.
No false positives.
- `oxfmt` and `oxlint` clean on the new script.

## Rollback

Pure tooling addition, no runtime code, no schema or data change. Revert
to remove.
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