From 0e84c608afece5e68ac61d35ce2ebe0e44f3c884 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 3 Nov 2025 23:15:55 -0800 Subject: [PATCH 1/5] Significantly faster users/[user_id] endpoint (and some others) --- .../backend/src/app/api/latest/users/crud.tsx | 27 +-- apps/backend/src/lib/tenancies.tsx | 166 +++++++++++++++--- .../src/route-handlers/smart-request.tsx | 11 +- 3 files changed, 160 insertions(+), 44 deletions(-) diff --git a/apps/backend/src/app/api/latest/users/crud.tsx b/apps/backend/src/app/api/latest/users/crud.tsx index f3ae7c015e..aecfead39d 100644 --- a/apps/backend/src/app/api/latest/users/crud.tsx +++ b/apps/backend/src/app/api/latest/users/crud.tsx @@ -1,8 +1,8 @@ -import { getRenderedEnvironmentConfigQuery } from "@/lib/config"; +import { getRenderedProjectConfigQuery } from "@/lib/config"; import { normalizeEmail } from "@/lib/emails"; import { grantDefaultProjectPermissions } from "@/lib/permissions"; import { ensureTeamMembershipExists, ensureUserExists } from "@/lib/request-checks"; -import { Tenancy, getSoleTenancyFromProjectBranch, getTenancy } from "@/lib/tenancies"; +import { Tenancy, getSoleTenancyFromProjectBranch } from "@/lib/tenancies"; import { PrismaTransaction } from "@/lib/types"; import { sendTeamMembershipDeletedWebhook, sendUserCreatedWebhook, sendUserDeletedWebhook, sendUserUpdatedWebhook } from "@/lib/webhooks"; import { RawQuery, getPrismaClientForSourceOfTruth, getPrismaClientForTenancy, getPrismaSchemaForSourceOfTruth, getPrismaSchemaForTenancy, globalPrismaClient, rawQuery, retryTransaction, sqlQuoteIdent } from "@/prisma-client"; @@ -388,20 +388,21 @@ export function getUserIfOnGlobalPrismaClientQuery(projectId: string, branchId: }; } -export async function getUser(options: { userId: string } & ({ projectId: string, branchId: string } | { tenancyId: string })) { - let projectId, branchId; - if (!("tenancyId" in options)) { +export async function getUser(options: { userId: string } & ({ projectId: string, branchId: string } | { tenancy: Tenancy })) { + let projectId, branchId, sourceOfTruth; + if ("tenancy" in options) { + projectId = options.tenancy.project.id; + branchId = options.tenancy.branchId; + sourceOfTruth = options.tenancy.config.sourceOfTruth; + } else { projectId = options.projectId; branchId = options.branchId; - } else { - const tenancy = await getTenancy(options.tenancyId) ?? throwErr("Tenancy not found", { tenancyId: options.tenancyId }); - projectId = tenancy.project.id; - branchId = tenancy.branchId; + const projectConfig = await rawQuery(globalPrismaClient, getRenderedProjectConfigQuery({ projectId })); + sourceOfTruth = projectConfig.sourceOfTruth; } - const environmentConfig = await rawQuery(globalPrismaClient, getRenderedEnvironmentConfigQuery({ projectId, branchId })); - const prisma = await getPrismaClientForSourceOfTruth(environmentConfig.sourceOfTruth, branchId); - const schema = await getPrismaSchemaForSourceOfTruth(environmentConfig.sourceOfTruth, branchId); + const prisma = await getPrismaClientForSourceOfTruth(sourceOfTruth, branchId); + const schema = await getPrismaSchemaForSourceOfTruth(sourceOfTruth, branchId); const result = await rawQuery(prisma, getUserQuery(projectId, branchId, options.userId, schema)); return result; } @@ -421,7 +422,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC include_anonymous: yupString().oneOf(["true", "false"]).optional().meta({ openapiField: { onlyShowInOperations: [ 'List' ], description: "Whether to include anonymous users in the results. Defaults to false" } }), }), onRead: async ({ auth, params, query }) => { - const user = await getUser({ tenancyId: auth.tenancy.id, userId: params.user_id }); + const user = await getUser({ tenancy: auth.tenancy, userId: params.user_id }); if (!user) { throw new KnownErrors.UserNotFound(); } diff --git a/apps/backend/src/lib/tenancies.tsx b/apps/backend/src/lib/tenancies.tsx index a2b877340a..31fb0a3122 100644 --- a/apps/backend/src/lib/tenancies.tsx +++ b/apps/backend/src/lib/tenancies.tsx @@ -1,9 +1,11 @@ -import { globalPrismaClient, rawQuery } from "@/prisma-client"; +import { globalPrismaClient, RawQuery, rawQuery } from "@/prisma-client"; import { Prisma } from "@prisma/client"; import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; +import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { deepPlainEquals } from "@stackframe/stack-shared/dist/utils/objects"; import { getRenderedOrganizationConfigQuery } from "./config"; -import { getProject } from "./projects"; +import { getProject, getProjectQuery } from "./projects"; /** * @deprecated YOU PROBABLY ALMOST NEVER WANT TO USE THIS, UNLESS YOU ACTUALLY NEED THE DEFAULT BRANCH ID. DON'T JUST USE THIS TO GET A TENANCY BECAUSE YOU DON'T HAVE ONE @@ -16,7 +18,11 @@ import { getProject } from "./projects"; */ export const DEFAULT_BRANCH_ID = "main"; -export async function tenancyPrismaToCrud(prisma: Prisma.TenancyGetPayload<{}>) { +/** + * @deprecated UNUSED: This function is only kept for development mode validation in getTenancyFromProject. + * The old Prisma-based implementation, replaced by getTenancyFromProjectQuery which uses RawQuery. + */ +async function tenancyPrismaToCrudUnused(prisma: Prisma.TenancyGetPayload<{}>) { if (prisma.hasNoOrganization && prisma.organizationId !== null) { throw new StackAssertionError("Organization ID is not null for a tenancy with hasNoOrganization", { tenancyId: prisma.id, prisma }); } @@ -44,7 +50,7 @@ export async function tenancyPrismaToCrud(prisma: Prisma.TenancyGetPayload<{}>) }; } -export type Tenancy = Awaited>; +export type Tenancy = Awaited>; /** * @deprecated This is a temporary function for the situation where every project-branch has exactly one tenancy. Later, @@ -57,7 +63,7 @@ export function getSoleTenancyFromProjectBranch(project: Omit | string, branchId: string, returnNullIfNotFound: boolean): Promise; export async function getSoleTenancyFromProjectBranch(projectOrId: Omit | string, branchId: string, returnNullIfNotFound: boolean = false): Promise { - const res = await getTenancyFromProject(typeof projectOrId === 'string' ? projectOrId : projectOrId.id, branchId, null); + const res = await rawQuery(globalPrismaClient, getSoleTenancyFromProjectBranchQuery(projectOrId, branchId, true)); if (!res) { if (returnNullIfNotFound) return null; throw new StackAssertionError(`No tenancy found for project ${typeof projectOrId === 'string' ? projectOrId : projectOrId.id}`, { projectOrId }); @@ -65,6 +71,14 @@ export async function getSoleTenancyFromProjectBranch(projectOrId: Omit | string, branchId: string, returnNullIfNotFound: true): RawQuery> { + return getTenancyFromProjectQuery(typeof project === 'string' ? project : project.id, branchId, null); +} + export async function getTenancy(tenancyId: string) { if (tenancyId === "internal") { throw new StackAssertionError("Tried to get tenancy with ID `internal`. This is a mistake because `internal` is only a valid identifier for projects."); @@ -73,31 +87,133 @@ export async function getTenancy(tenancyId: string) { where: { id: tenancyId }, }); if (!prisma) return null; - return await tenancyPrismaToCrud(prisma); + return await getTenancyFromProject(prisma.projectId, prisma.branchId, prisma.organizationId); +} + +function getTenancyFromProjectQuery(projectId: string, branchId: string, organizationId: string | null): RawQuery> { + return RawQuery.then( + RawQuery.all([ + { + supportedPrismaClients: ["global"], + sql: organizationId === null + ? Prisma.sql` + SELECT "Tenancy".* + FROM "Tenancy" + WHERE "Tenancy"."projectId" = ${projectId} + AND "Tenancy"."branchId" = ${branchId} + AND "Tenancy"."hasNoOrganization" = 'TRUE' + ` + : Prisma.sql` + SELECT "Tenancy".* + FROM "Tenancy" + WHERE "Tenancy"."projectId" = ${projectId} + AND "Tenancy"."branchId" = ${branchId} + AND "Tenancy"."organizationId" = ${organizationId} + `, + postProcess: (queryResult) => { + if (queryResult.length > 1) { + throw new StackAssertionError( + `Expected 0 or 1 tenancies for project ${projectId}, branch ${branchId}, organization ${organizationId}, got ${queryResult.length}`, + { queryResult } + ); + } + if (queryResult.length === 0) { + return Promise.resolve(null); + } + return Promise.resolve(queryResult[0] as Prisma.TenancyGetPayload<{}>); + }, + }, + getProjectQuery(projectId), + getRenderedOrganizationConfigQuery({ + projectId, + branchId, + organizationId, + }), + ] as const), + async ([tenancyResultPromise, projectResultPromise, configPromise]) => { + const [tenancyResult, projectResult, config] = await Promise.all([ + tenancyResultPromise, + projectResultPromise, + configPromise, + ]); + + if (!tenancyResult) return null; + if (!projectResult) { + throw new StackAssertionError("Project in tenancy not found", { projectId, tenancyId: tenancyResult.id }); + } + + // Validate tenancy consistency + if (tenancyResult.hasNoOrganization && tenancyResult.organizationId !== null) { + throw new StackAssertionError("Organization ID is not null for a tenancy with hasNoOrganization", { + tenancyId: tenancyResult.id, + tenancy: tenancyResult + }); + } + if (!tenancyResult.hasNoOrganization && tenancyResult.organizationId === null) { + throw new StackAssertionError("Organization ID is null for a tenancy without hasNoOrganization", { + tenancyId: tenancyResult.id, + tenancy: tenancyResult + }); + } + + return { + id: tenancyResult.id, + config, + branchId: tenancyResult.branchId, + organization: tenancyResult.organizationId === null ? null : { + // TODO actual organization type + id: tenancyResult.organizationId, + }, + project: projectResult, + }; + } + ); } /** * @deprecated Not actually deprecated but if you're using this you're probably doing something wrong — ask Konsti for help + * + * (if Konsti is not around — unless you are editing the implementation of SmartRequestAuth, you should probably take the + * tenancy from the SmartRequest auth parameter instead of fetching your own. If you are editing the SmartRequestAuth + * implementation — carry on.) */ export async function getTenancyFromProject(projectId: string, branchId: string, organizationId: string | null) { - const prisma = await globalPrismaClient.tenancy.findUnique({ - where: { - ...(organizationId === null ? { - projectId_branchId_hasNoOrganization: { - projectId: projectId, - branchId: branchId, - hasNoOrganization: "TRUE", - } - } : { - projectId_branchId_organizationId: { - projectId: projectId, - branchId: branchId, - organizationId: organizationId, - } - }), - }, - }); - if (!prisma) return null; - return await tenancyPrismaToCrud(prisma); + // Use the new RawQuery implementation + const result = await rawQuery(globalPrismaClient, getTenancyFromProjectQuery(projectId, branchId, organizationId)); + + // In development mode, compare with the old implementation to ensure correctness + if (!!getNodeEnvironment().includes("prod")) { + const prisma = await globalPrismaClient.tenancy.findUnique({ + where: { + ...(organizationId === null ? { + projectId_branchId_hasNoOrganization: { + projectId: projectId, + branchId: branchId, + hasNoOrganization: "TRUE", + } + } : { + projectId_branchId_organizationId: { + projectId: projectId, + branchId: branchId, + organizationId: organizationId, + } + }), + }, + }); + const oldResult = prisma ? await tenancyPrismaToCrudUnused(prisma) : null; + + // Compare the two results + if (!deepPlainEquals(result, oldResult)) { + throw new StackAssertionError("getTenancyFromProject: new implementation does not match old implementation", { + projectId, + branchId, + organizationId, + newResult: result, + oldResult, + }); + } + } + + return result; } diff --git a/apps/backend/src/route-handlers/smart-request.tsx b/apps/backend/src/route-handlers/smart-request.tsx index 5309268247..3c9eee18d4 100644 --- a/apps/backend/src/route-handlers/smart-request.tsx +++ b/apps/backend/src/route-handlers/smart-request.tsx @@ -4,7 +4,7 @@ import { getUser, getUserIfOnGlobalPrismaClientQuery } from "@/app/api/latest/us import { getRenderedEnvironmentConfigQuery } from "@/lib/config"; import { checkApiKeySet, checkApiKeySetQuery } from "@/lib/internal-api-keys"; import { getProjectQuery, listManagedProjectIds } from "@/lib/projects"; -import { DEFAULT_BRANCH_ID, Tenancy, getSoleTenancyFromProjectBranch } from "@/lib/tenancies"; +import { DEFAULT_BRANCH_ID, Tenancy, getSoleTenancyFromProjectBranchQuery } from "@/lib/tenancies"; import { decodeAccessToken } from "@/lib/tokens"; import { globalPrismaClient, rawQueryAll } from "@/prisma-client"; import { KnownErrors } from "@stackframe/stack-shared"; @@ -248,11 +248,13 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque isServerKeyValid: secretServerKey && requestType === "server" ? checkApiKeySetQuery(projectId, { secretServerKey }) : undefined, isAdminKeyValid: superSecretAdminKey && requestType === "admin" ? checkApiKeySetQuery(projectId, { superSecretAdminKey }) : undefined, project: getProjectQuery(projectId), + tenancy: getSoleTenancyFromProjectBranchQuery(projectId, branchId, true), environmentRenderedConfig: getRenderedEnvironmentConfigQuery({ projectId, branchId }), }; const queriesResults = await rawQueryAll(globalPrismaClient, bundledQueries); const project = await queriesResults.project; - if (project === null) throw new KnownErrors.CurrentProjectNotFound(projectId); // this does allow one to probe whether a project exists or not, but that's fine + if (project === null) throw new KnownErrors.CurrentProjectNotFound(projectId); // this does allow one to probe whether a project exists or not, but that's fine (it's worth the better error messages) + const tenancy = await queriesResults.tenancy; const environmentConfig = await queriesResults.environmentRenderedConfig; // As explained above, as a performance optimization we already fetch the user from the global database optimistically @@ -262,10 +264,6 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque ? queriesResults.userIfOnGlobalPrismaClient : (userId ? await getUser({ userId, projectId, branchId }) : undefined); - // TODO HACK tenancy is not needed for /users/me, so let's not fetch it as a hack to make the endpoint faster. Once we - // refactor this stuff, we can fetch the tenancy alongside the user and won't need this anymore - const tenancy = req.method === "GET" && req.url.endsWith("/users/me") ? "tenancy not available in /users/me as a performance hack" as never : await getSoleTenancyFromProjectBranch(projectId, branchId, true); - if (developmentKeyOverride) { if (!["development", "test"].includes(getNodeEnvironment()) && getEnvVariable("STACK_ALLOW_DEVELOPMENT_KEY_OVERRIDE_DESPITE_PRODUCTION", "") !== "this-is-dangerous") { // it's not actually that dangerous, but it changes the security model throw new StatusError(401, "Development key override is only allowed in development or test environments"); @@ -299,6 +297,7 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque } if (!tenancy) { + // note that we only check branch existence here so you can't probe branches unless you have a project ID throw new KnownErrors.BranchDoesNotExist(branchId); } From 65c8f9b701dbf65c8ecd278c5bffc9dc8cc5b413 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 3 Nov 2025 23:19:22 -0800 Subject: [PATCH 2/5] fix --- apps/backend/src/lib/tenancies.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/lib/tenancies.tsx b/apps/backend/src/lib/tenancies.tsx index 31fb0a3122..d45c99ca91 100644 --- a/apps/backend/src/lib/tenancies.tsx +++ b/apps/backend/src/lib/tenancies.tsx @@ -182,7 +182,7 @@ export async function getTenancyFromProject(projectId: string, branchId: string, const result = await rawQuery(globalPrismaClient, getTenancyFromProjectQuery(projectId, branchId, organizationId)); // In development mode, compare with the old implementation to ensure correctness - if (!!getNodeEnvironment().includes("prod")) { + if (!getNodeEnvironment().includes("prod")) { const prisma = await globalPrismaClient.tenancy.findUnique({ where: { ...(organizationId === null ? { From e42b3b91fe29edccf620d4a7eb4698e62ae3f8cf Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 3 Nov 2025 23:36:58 -0800 Subject: [PATCH 3/5] fix --- apps/backend/src/lib/config.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/backend/src/lib/config.tsx b/apps/backend/src/lib/config.tsx index d833c192e9..56e792161f 100644 --- a/apps/backend/src/lib/config.tsx +++ b/apps/backend/src/lib/config.tsx @@ -131,10 +131,7 @@ export function getProjectConfigOverrideQuery(options: ProjectOptions): RawQuery if (queryResult.length > 1) { throw new StackAssertionError(`Expected 0 or 1 project config overrides for project ${options.projectId}, got ${queryResult.length}`, { queryResult }); } - if (queryResult.length === 0) { - throw new StackAssertionError(`Expected a project row for project ${options.projectId}, got 0`, { queryResult, options }); - } - return migrateConfigOverride("project", queryResult[0].projectConfigOverride ?? {}); + return migrateConfigOverride("project", queryResult[0]?.projectConfigOverride ?? {}); }, }; } From f044989f78705b2e1f5164ec57979b4278705684 Mon Sep 17 00:00:00 2001 From: Konsti Wohlwend Date: Tue, 4 Nov 2025 10:37:18 -0800 Subject: [PATCH 4/5] Refactor tenancy result handling in tenancies.tsx Refactor to await tenancyResultPromise separately and check for null. --- apps/backend/src/lib/tenancies.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/lib/tenancies.tsx b/apps/backend/src/lib/tenancies.tsx index d45c99ca91..94c7724679 100644 --- a/apps/backend/src/lib/tenancies.tsx +++ b/apps/backend/src/lib/tenancies.tsx @@ -131,13 +131,15 @@ function getTenancyFromProjectQuery(projectId: string, branchId: string, organiz }), ] as const), async ([tenancyResultPromise, projectResultPromise, configPromise]) => { - const [tenancyResult, projectResult, config] = await Promise.all([ - tenancyResultPromise, + const tenancyResult = await tenancyResultPromise; + + if (!tenancyResult) return null; + + const [projectResult, config] = await Promise.all([ projectResultPromise, configPromise, ]); - - if (!tenancyResult) return null; + if (!projectResult) { throw new StackAssertionError("Project in tenancy not found", { projectId, tenancyId: tenancyResult.id }); } From 05508b340d25070b0601421c6e9b068336e73a65 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Tue, 4 Nov 2025 22:04:58 -0800 Subject: [PATCH 5/5] Fix tests --- apps/backend/src/lib/config.tsx | 6 +- apps/backend/src/lib/tenancies.tsx | 2 +- .../src/route-handlers/smart-request.tsx | 17 +++--- apps/e2e/tests/backend/backend-helpers.ts | 1 + .../backend/endpoints/api/v1/index.test.ts | 60 +++++++++++++++++++ .../backend/endpoints/api/v1/projects.test.ts | 2 +- 6 files changed, 74 insertions(+), 14 deletions(-) diff --git a/apps/backend/src/lib/config.tsx b/apps/backend/src/lib/config.tsx index 56e792161f..0838f3d466 100644 --- a/apps/backend/src/lib/config.tsx +++ b/apps/backend/src/lib/config.tsx @@ -139,13 +139,13 @@ export function getProjectConfigOverrideQuery(options: ProjectOptions): RawQuery export function getBranchConfigOverrideQuery(options: BranchOptions): RawQuery> { // fetch branch config from GitHub // (currently it's just empty) - if (options.branchId !== DEFAULT_BRANCH_ID) { - throw new StackAssertionError('Not implemented'); - } return { supportedPrismaClients: ["global"], sql: Prisma.sql`SELECT 1`, postProcess: async () => { + if (options.branchId !== DEFAULT_BRANCH_ID) { + throw new StackAssertionError('getBranchConfigOverrideQuery is not implemented for branches other than the default one'); + } return migrateConfigOverride("branch", {}); }, }; diff --git a/apps/backend/src/lib/tenancies.tsx b/apps/backend/src/lib/tenancies.tsx index 94c7724679..19070d4084 100644 --- a/apps/backend/src/lib/tenancies.tsx +++ b/apps/backend/src/lib/tenancies.tsx @@ -139,7 +139,7 @@ function getTenancyFromProjectQuery(projectId: string, branchId: string, organiz projectResultPromise, configPromise, ]); - + if (!projectResult) { throw new StackAssertionError("Project in tenancy not found", { projectId, tenancyId: tenancyResult.id }); } diff --git a/apps/backend/src/route-handlers/smart-request.tsx b/apps/backend/src/route-handlers/smart-request.tsx index 3c9eee18d4..045896b3b6 100644 --- a/apps/backend/src/route-handlers/smart-request.tsx +++ b/apps/backend/src/route-handlers/smart-request.tsx @@ -255,14 +255,6 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque const project = await queriesResults.project; if (project === null) throw new KnownErrors.CurrentProjectNotFound(projectId); // this does allow one to probe whether a project exists or not, but that's fine (it's worth the better error messages) const tenancy = await queriesResults.tenancy; - const environmentConfig = await queriesResults.environmentRenderedConfig; - - // As explained above, as a performance optimization we already fetch the user from the global database optimistically - // If it turned out that the source-of-truth is not the global database, we'll fetch the user from the source-of-truth - // database instead. - const user = environmentConfig.sourceOfTruth.type === "hosted" - ? queriesResults.userIfOnGlobalPrismaClient - : (userId ? await getUser({ userId, projectId, branchId }) : undefined); if (developmentKeyOverride) { if (!["development", "test"].includes(getNodeEnvironment()) && getEnvVariable("STACK_ALLOW_DEVELOPMENT_KEY_OVERRIDE_DESPITE_PRODUCTION", "") !== "this-is-dangerous") { // it's not actually that dangerous, but it changes the security model @@ -297,10 +289,17 @@ const parseAuth = withTraceSpan('smart request parseAuth', async (req: NextReque } if (!tenancy) { - // note that we only check branch existence here so you can't probe branches unless you have a project ID + // note that we only check branch existence here so you can't probe branches unless you have the project keys throw new KnownErrors.BranchDoesNotExist(branchId); } + // As explained above, as a performance optimization we already fetch the user from the global database optimistically + // If it turned out that the source-of-truth is not the global database, we'll fetch the user from the source-of-truth + // database instead. + const user = tenancy.config.sourceOfTruth.type === "hosted" + ? queriesResults.userIfOnGlobalPrismaClient + : (userId ? await getUser({ userId, projectId, branchId }) : undefined); + return { project, branchId, diff --git a/apps/e2e/tests/backend/backend-helpers.ts b/apps/e2e/tests/backend/backend-helpers.ts index 857f9a332b..002424d553 100644 --- a/apps/e2e/tests/backend/backend-helpers.ts +++ b/apps/e2e/tests/backend/backend-helpers.ts @@ -67,6 +67,7 @@ export function createMailbox(email?: string): Mailbox { export type ProjectKeys = "no-project" | { projectId: string, + branchId?: string, publishableClientKey?: string, secretServerKey?: string, superSecretAdminKey?: string, diff --git a/apps/e2e/tests/backend/endpoints/api/v1/index.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/index.test.ts index 50fd3ee101..0f125a1a98 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/index.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/index.test.ts @@ -76,6 +76,66 @@ describe("without project ID", () => { it.todo("should not be able to authenticate as user"); }); +describe("with project ID that doesn't exist", async () => { + backendContext.set({ + projectKeys: { + projectId: "invalid", + publishableClientKey: "publish-key", + secretServerKey: "secret-key", + superSecretAdminKey: "admin-key", + } + }); + + it("should not have client access", async ({ expect }) => { + const response = await niceBackendFetch("/api/v1", { + accessType: "client", + }); + expect(response).toMatchInlineSnapshot(` + NiceResponse { + "status": 400, + "body": { + "code": "CURRENT_PROJECT_NOT_FOUND", + "details": { "project_id": "invalid" }, + "error": "The current project with ID invalid was not found. Please check the value of the x-stack-project-id header.", + }, + "headers": Headers { + "x-stack-known-error": "CURRENT_PROJECT_NOT_FOUND", +