From 37e436d998c22f5a08f4da767ade5c66b87362a0 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 11 Jun 2026 15:53:52 +0100 Subject: [PATCH 1/2] fix(webapp): only load env var values for displayed environments The environment variables page loaded every value in the project, including rows left behind by archived preview-branch environments, which made the loader extremely slow for projects that churn branches. The values relation is now filtered to the environments the page actually displays. --- ...ge-scope-values-to-visible-environments.md | 6 + .../EnvironmentVariablesPresenter.server.ts | 23 ++-- .../EnvironmentVariablesPresenter.test.ts | 108 ++++++++++++++++++ 3 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 .server-changes/env-vars-page-scope-values-to-visible-environments.md diff --git a/.server-changes/env-vars-page-scope-values-to-visible-environments.md b/.server-changes/env-vars-page-scope-values-to-visible-environments.md new file mode 100644 index 00000000000..067c04661b7 --- /dev/null +++ b/.server-changes/env-vars-page-scope-values-to-visible-environments.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Speed up the environment variables page for projects with many archived preview branches. The page now only loads variable values for the environments it displays instead of every value ever created, including those left behind by archived branches. diff --git a/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts b/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts index 2ab7f1f5a1b..646d07ac5c3 100644 --- a/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts @@ -41,6 +41,17 @@ export class EnvironmentVariablesPresenter { throw new Error("Project not found"); } + const { environments: sortedEnvironments, hasStaging } = + await loadEnvironmentVariablesEnvironments( + this.#prismaClient, + { userId, projectId: project.id }, + { skipProjectAccessCheck: true } + ); + + // Only load values for the environments we display. Projects can accumulate + // values in archived branch environments, which would otherwise all be loaded here. + const environmentIds = sortedEnvironments.map((env) => env.id); + const environmentVariables = await this.#prismaClient.environmentVariable.findMany({ select: { id: true, @@ -59,6 +70,11 @@ export class EnvironmentVariablesPresenter { }, isSecret: true, }, + where: { + environmentId: { + in: environmentIds, + }, + }, }, }, where: { @@ -102,13 +118,6 @@ export class EnvironmentVariablesPresenter { const usersRecord: Record = Object.fromEntries(users.map((u) => [u.id, u])); - const { environments: sortedEnvironments, hasStaging } = - await loadEnvironmentVariablesEnvironments( - this.#prismaClient, - { userId, projectId: project.id }, - { skipProjectAccessCheck: true } - ); - const repository = new EnvironmentVariablesRepository(this.#prismaClient); const nonSecretItems: Array<{ environmentId: string; key: string }> = []; diff --git a/apps/webapp/test/EnvironmentVariablesPresenter.test.ts b/apps/webapp/test/EnvironmentVariablesPresenter.test.ts index 1fd2c9e9619..aa593f94499 100644 --- a/apps/webapp/test/EnvironmentVariablesPresenter.test.ts +++ b/apps/webapp/test/EnvironmentVariablesPresenter.test.ts @@ -69,4 +69,112 @@ describe("EnvironmentVariablesPresenter", () => { expect(secretVariable!.value).toBe(""); expect(nonSecretVariable!.value).toBe("plain-value"); }); + + postgresTest( + "returns values for active environments (including branch environments) and excludes archived branch environments", + async ({ prisma }) => { + const { user, organization, project, projectSlug } = + await createTestOrgProjectWithMember(prisma); + + const prodEnvironment = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PRODUCTION", + }); + + const parentPreviewEnvironment = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PREVIEW", + }); + await prisma.runtimeEnvironment.update({ + where: { id: parentPreviewEnvironment.id }, + data: { isBranchableEnvironment: true }, + }); + + const activeBranchEnvironment = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PREVIEW", + }); + await prisma.runtimeEnvironment.update({ + where: { id: activeBranchEnvironment.id }, + data: { + parentEnvironmentId: parentPreviewEnvironment.id, + branchName: "feature/active", + }, + }); + + const archivedBranchEnvironment = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PREVIEW", + }); + await prisma.runtimeEnvironment.update({ + where: { id: archivedBranchEnvironment.id }, + data: { + parentEnvironmentId: parentPreviewEnvironment.id, + branchName: "feature/archived", + }, + }); + + const repository = new EnvironmentVariablesRepository(prisma, prisma); + + await createEnvironmentVariable(repository, project.id, { + environmentId: prodEnvironment.id, + key: "MY_VAR", + value: "prod-value", + userId: user.id, + }); + await createEnvironmentVariable(repository, project.id, { + environmentId: activeBranchEnvironment.id, + key: "MY_VAR", + value: "active-branch-value", + userId: user.id, + }); + await createEnvironmentVariable(repository, project.id, { + environmentId: archivedBranchEnvironment.id, + key: "MY_VAR", + value: "archived-branch-value", + userId: user.id, + }); + + // Archive the branch after it accumulated values (archiving does not + // delete its EnvironmentVariableValue rows). + await prisma.runtimeEnvironment.update({ + where: { id: archivedBranchEnvironment.id }, + data: { archivedAt: new Date() }, + }); + + const result = await new EnvironmentVariablesPresenter(prisma).call({ + userId: user.id, + projectSlug, + }); + + const environmentIds = result.environments.map((environment) => environment.id); + expect(environmentIds).toContain(prodEnvironment.id); + expect(environmentIds).toContain(activeBranchEnvironment.id); + expect(environmentIds).not.toContain(archivedBranchEnvironment.id); + + const myVarValues = result.environmentVariables.filter( + (variable) => variable.key === "MY_VAR" + ); + expect(myVarValues).toHaveLength(2); + + const prodValue = myVarValues.find( + (variable) => variable.environment.id === prodEnvironment.id + ); + expect(prodValue?.value).toBe("prod-value"); + + const activeBranchValue = myVarValues.find( + (variable) => variable.environment.id === activeBranchEnvironment.id + ); + expect(activeBranchValue?.value).toBe("active-branch-value"); + expect(activeBranchValue?.environment.branchName).toBe("feature/active"); + + expect( + myVarValues.some((variable) => variable.environment.id === archivedBranchEnvironment.id) + ).toBe(false); + } + ); }); From d534aa94f0855b421027649a58d6dc05e36ba053 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 11 Jun 2026 18:04:13 +0100 Subject: [PATCH 2/2] fix(webapp): read environment variables page data from the replica --- .../v3/EnvironmentVariablesPresenter.server.ts | 16 +++++++++------- .../environmentVariablesEnvironments.server.ts | 4 ++-- .../environmentVariablesRepository.server.ts | 2 +- .../test/EnvironmentVariablesPresenter.test.ts | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts b/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts index 646d07ac5c3..720bfda8db7 100644 --- a/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts @@ -1,4 +1,4 @@ -import { PrismaClient, prisma } from "~/db.server"; +import { $replica, PrismaClient, PrismaReplicaClient, prisma } from "~/db.server"; import { Project } from "~/models/project.server"; import { User } from "~/models/user.server"; import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server"; @@ -15,13 +15,15 @@ export type EnvironmentVariableWithSetValues = Result["environmentVariables"][nu export class EnvironmentVariablesPresenter { #prismaClient: PrismaClient; + #replicaClient: PrismaReplicaClient; - constructor(prismaClient: PrismaClient = prisma) { + constructor(prismaClient: PrismaClient = prisma, replicaClient: PrismaReplicaClient = $replica) { this.#prismaClient = prismaClient; + this.#replicaClient = replicaClient; } public async call({ userId, projectSlug }: { userId: User["id"]; projectSlug: Project["slug"] }) { - const project = await this.#prismaClient.project.findFirst({ + const project = await this.#replicaClient.project.findFirst({ select: { id: true, }, @@ -43,7 +45,7 @@ export class EnvironmentVariablesPresenter { const { environments: sortedEnvironments, hasStaging } = await loadEnvironmentVariablesEnvironments( - this.#prismaClient, + this.#replicaClient, { userId, projectId: project.id }, { skipProjectAccessCheck: true } ); @@ -52,7 +54,7 @@ export class EnvironmentVariablesPresenter { // values in archived branch environments, which would otherwise all be loaded here. const environmentIds = sortedEnvironments.map((env) => env.id); - const environmentVariables = await this.#prismaClient.environmentVariable.findMany({ + const environmentVariables = await this.#replicaClient.environmentVariable.findMany({ select: { id: true, key: true, @@ -100,7 +102,7 @@ export class EnvironmentVariablesPresenter { const users = userIds.size > 0 - ? await this.#prismaClient.user.findMany({ + ? await this.#replicaClient.user.findMany({ where: { id: { in: Array.from(userIds), @@ -118,7 +120,7 @@ export class EnvironmentVariablesPresenter { const usersRecord: Record = Object.fromEntries(users.map((u) => [u.id, u])); - const repository = new EnvironmentVariablesRepository(this.#prismaClient); + const repository = new EnvironmentVariablesRepository(this.#prismaClient, this.#replicaClient); const nonSecretItems: Array<{ environmentId: string; key: string }> = []; for (const environmentVariable of environmentVariables) { diff --git a/apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts b/apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts index 218d0be7eb6..9473127df05 100644 --- a/apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts +++ b/apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts @@ -1,5 +1,5 @@ import type { RuntimeEnvironmentType } from "@trigger.dev/database"; -import { type PrismaClient } from "~/db.server"; +import { type PrismaReplicaClient } from "~/db.server"; import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmentSort"; export type EnvironmentVariablesEnvironment = { @@ -15,7 +15,7 @@ export type EnvironmentVariablesEnvironmentsResult = { }; export async function loadEnvironmentVariablesEnvironments( - prismaClient: PrismaClient, + prismaClient: PrismaReplicaClient, { userId, projectId }: { userId: string; projectId: string }, options?: { skipProjectAccessCheck?: boolean } ): Promise { diff --git a/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts b/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts index 4a8e0e36c0a..68f9aa0f3b3 100644 --- a/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts +++ b/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts @@ -591,7 +591,7 @@ export class EnvironmentVariablesRepository implements Repository { } const secretStore = getSecretStore("DATABASE", { - prismaClient: this.prismaClient, + prismaClient: this.replicaClient, }); const storeKeys = Array.from(uniqueItems.values()).map((item) => diff --git a/apps/webapp/test/EnvironmentVariablesPresenter.test.ts b/apps/webapp/test/EnvironmentVariablesPresenter.test.ts index aa593f94499..b5172bd5948 100644 --- a/apps/webapp/test/EnvironmentVariablesPresenter.test.ts +++ b/apps/webapp/test/EnvironmentVariablesPresenter.test.ts @@ -56,7 +56,7 @@ describe("EnvironmentVariablesPresenter", () => { userId: user.id, }); - const result = await new EnvironmentVariablesPresenter(prisma).call({ + const result = await new EnvironmentVariablesPresenter(prisma, prisma).call({ userId: user.id, projectSlug, }); @@ -146,7 +146,7 @@ describe("EnvironmentVariablesPresenter", () => { data: { archivedAt: new Date() }, }); - const result = await new EnvironmentVariablesPresenter(prisma).call({ + const result = await new EnvironmentVariablesPresenter(prisma, prisma).call({ userId: user.id, projectSlug, });