perf(database): index PersonalAccessToken.userId so token lookups stop seq-scanning - #4588
Conversation
|
WalkthroughThe change adds an index on 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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.
b10055b to
d8145db
Compare
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
.server-changes/index-personal-access-token-user-id.mdinternal-packages/database/prisma/migrations/20260812140000_add_personal_access_token_user_id_index/migration.sqlinternal-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, useCREATE INDEX CONCURRENTLY IF NOT EXISTSto avoid production table locks.
Keep eachCONCURRENTLYindex in its own separate migration file, and add only one index per migration file.
Indexes on newly created tables may be created withoutCONCURRENTLYin the same migration asCREATE TABLE.
When adding an index for a new column on an existing table, use two migrations: firstALTER TABLE ... ADD COLUMN IF NOT EXISTS ..., then a separate migration containingCREATE 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 inprisma/schema.prismausing Prisma 6.14.0 for PostgreSQL.
New code must always targetRunEngineVersion.V2;V1is 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.sqlinternal-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.sqlinternal-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.sqlinternal-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.sqlinternal-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.sqlinternal-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.sqlinternal-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.sqlinternal-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 & AvailabilityTest the Prisma 6.14.0 migration path.
Run
prisma migrate deployagainst a disposable PostgreSQL database.CREATE INDEX CONCURRENTLYmust execute outside a transaction. Keep this statement as the only statement in this migration.
## 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>
## 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.
Summary
The two personal-access-token lookups by
userId(one also filteringrevokedAt is null, the other also filteringname) had no index onuserId, so each did a full sequential scan of thePersonalAccessTokentable to return a single row.userIdis also an unindexed foreign key.Fix
Add a single
@@index([userId]). A user owns only a handful of PATs, so onceuserIdis indexed each lookup touches a few rows and the residualrevokedAt/namefilter is trivial. Both query shapes lead withuserId =, so one index serves both and a composite would only add write cost. The migration usesCREATE 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.