Compare commits

...

2 Commits

Author SHA1 Message Date
Johannes 056a019738 env references to clean up 2026-04-19 12:35:53 +02:00
Bhagya Amarasinghe 9d2e988c59 feat: remove app rate limits for Envoy-covered routes (#7714)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2026-04-17 12:43:22 +04:00
11 changed files with 988 additions and 109 deletions
+1 -3
View File
@@ -157,9 +157,7 @@ const handleApiKeyAuthentication = async (apiKey: string) => {
});
}
const rateLimitError = await checkRateLimit(apiKeyData.id);
if (rateLimitError) return rateLimitError;
// Rate limiting for apiKey auth is enforced by Envoy in v5 — see envoy-rate-limit-coverage.ts
if (!isValidApiKeyEnvironment(apiKeyData)) {
return responses.badRequestResponse("You can't use this method with this API key");
}
+140 -19
View File
@@ -3,9 +3,16 @@ import { NextRequest } from "next/server";
import { Mock, beforeEach, describe, expect, test, vi } from "vitest";
import { logger } from "@formbricks/logger";
import { TAuthenticationApiKey } from "@formbricks/types/auth";
import { AuthenticationMethod } from "@/app/middleware/endpoint-validator";
import type { AuthenticationMethod } from "@/app/middleware/endpoint-validator";
import { responses } from "./response";
const AuthMethod = {
ApiKey: "apiKey" as AuthenticationMethod,
Session: "session" as AuthenticationMethod,
Both: "both" as AuthenticationMethod,
None: "none" as AuthenticationMethod,
} as const;
vi.mock("@/modules/ee/audit-logs/lib/handler", () => ({
__esModule: true,
queueAuditEvent: vi.fn(),
@@ -122,7 +129,7 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.ApiKey,
authenticationMethod: AuthMethod.ApiKey,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
@@ -198,7 +205,7 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.ApiKey,
authenticationMethod: AuthMethod.ApiKey,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
@@ -244,7 +251,7 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.ApiKey,
authenticationMethod: AuthMethod.ApiKey,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
@@ -318,7 +325,7 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.ApiKey,
authenticationMethod: AuthMethod.ApiKey,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
@@ -370,7 +377,7 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.ApiKey,
authenticationMethod: AuthMethod.ApiKey,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
@@ -425,7 +432,7 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.ApiKey,
authenticationMethod: AuthMethod.ApiKey,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
@@ -449,7 +456,7 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: true, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: false,
authenticationMethod: AuthenticationMethod.None,
authenticationMethod: AuthMethod.None,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
vi.mocked(authenticateRequest).mockResolvedValue(null);
@@ -473,6 +480,90 @@ describe("withV1ApiWrapper", () => {
});
});
test("skips app rate limiting for Envoy-covered client routes", async () => {
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { applyIPRateLimit } = await import("@/modules/core/rate-limit/helpers");
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: true, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: false,
authenticationMethod: AuthMethod.None,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
vi.mocked(authenticateRequest).mockResolvedValue(null);
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
const handler = vi.fn().mockResolvedValue({
response: responses.successResponse({ data: "test" }),
});
const req = createMockRequest({ method: "POST", url: "/api/v1/client/env_123/storage" });
const { withV1ApiWrapper } = await import("./with-api-logging");
const wrapped = withV1ApiWrapper({ handler });
const res = await wrapped(req, undefined);
expect(res.status).toBe(200);
expect(applyIPRateLimit).not.toHaveBeenCalled();
});
test("keeps app rate limiting for uncovered client routes", async () => {
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { applyIPRateLimit } = await import("@/modules/core/rate-limit/helpers");
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: true, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: false,
authenticationMethod: AuthMethod.None,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
vi.mocked(authenticateRequest).mockResolvedValue(null);
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
const handler = vi.fn().mockResolvedValue({
response: responses.successResponse({ data: "test" }),
});
const req = createMockRequest({ method: "GET", url: "/api/v2/client/env_123/environment" });
const { withV1ApiWrapper } = await import("./with-api-logging");
const wrapped = withV1ApiWrapper({ handler });
const res = await wrapped(req, undefined);
expect(res.status).toBe(200);
expect(applyIPRateLimit).toHaveBeenCalled();
});
test("keeps app rate limiting for uncovered verbs on otherwise covered client paths", async () => {
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { applyIPRateLimit } = await import("@/modules/core/rate-limit/helpers");
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: true, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: false,
authenticationMethod: AuthMethod.None,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
vi.mocked(authenticateRequest).mockResolvedValue(null);
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
const handler = vi.fn().mockResolvedValue({
response: responses.successResponse({ data: "test" }),
});
const req = createMockRequest({ method: "PATCH", url: "/api/v1/client/env_123/environment" });
const { withV1ApiWrapper } = await import("./with-api-logging");
const wrapped = withV1ApiWrapper({ handler });
const res = await wrapped(req, undefined);
expect(res.status).toBe(200);
expect(applyIPRateLimit).toHaveBeenCalled();
});
test("returns authentication error for non-client routes without auth", async () => {
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
@@ -481,7 +572,7 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.ApiKey,
authenticationMethod: AuthMethod.ApiKey,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
vi.mocked(authenticateRequest).mockResolvedValue(null);
@@ -504,7 +595,7 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.Session,
authenticationMethod: AuthMethod.Session,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
vi.mocked(getServerSession).mockResolvedValue(null);
@@ -528,7 +619,36 @@ describe("withV1ApiWrapper", () => {
expect(mockContextualLoggerError).toHaveBeenCalled();
});
test("handles rate limiting errors", async () => {
test("keeps app rate limiting for uncovered session-authenticated management routes", async () => {
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { getServerSession } = await import("next-auth");
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthMethod.Both,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-1" } } as any);
const rateLimitError = new Error("Rate limit exceeded");
rateLimitError.message = "Rate limit exceeded";
vi.mocked(applyRateLimit).mockRejectedValue(rateLimitError);
const handler = vi.fn();
const req = createMockRequest({ method: "POST", url: "https://api.test/api/v1/management/storage" });
const { withV1ApiWrapper } = await import("./with-api-logging");
const customRateLimitConfig = { interval: 60, allowedPerInterval: 5, namespace: "storage:upload" };
const wrapped = withV1ApiWrapper({ handler, customRateLimitConfig });
const res = await wrapped(req, undefined);
expect(res.status).toBe(429);
expect(handler).not.toHaveBeenCalled();
expect(applyRateLimit).toHaveBeenCalledWith(customRateLimitConfig, "user-1");
});
test("skips app rate limiting for Envoy-covered API-key management routes", async () => {
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
@@ -538,21 +658,22 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.ApiKey,
authenticationMethod: AuthMethod.ApiKey,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
const rateLimitError = new Error("Rate limit exceeded");
rateLimitError.message = "Rate limit exceeded";
vi.mocked(applyRateLimit).mockRejectedValue(rateLimitError);
vi.mocked(applyRateLimit).mockResolvedValue({ allowed: true });
const handler = vi.fn().mockResolvedValue({
response: responses.successResponse({ data: "test" }),
});
const handler = vi.fn();
const req = createMockRequest({ url: V1_MANAGEMENT_SURVEYS_URL });
const { withV1ApiWrapper } = await import("./with-api-logging");
const wrapped = withV1ApiWrapper({ handler });
const res = await wrapped(req, undefined);
expect(res.status).toBe(429);
expect(handler).not.toHaveBeenCalled();
expect(res.status).toBe(200);
expect(applyRateLimit).not.toHaveBeenCalled();
});
test("skips audit log creation when no action/targetType provided", async () => {
@@ -566,7 +687,7 @@ describe("withV1ApiWrapper", () => {
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
vi.mocked(isManagementApiRoute).mockReturnValue({
isManagementApi: true,
authenticationMethod: AuthenticationMethod.ApiKey,
authenticationMethod: AuthMethod.ApiKey,
});
vi.mocked(isIntegrationRoute).mockReturnValue(false);
+44 -6
View File
@@ -13,6 +13,10 @@ import {
} from "@/app/middleware/endpoint-validator";
import { AUDIT_LOG_ENABLED } from "@/lib/constants";
import { authOptions } from "@/modules/auth/lib/authOptions";
import {
TEnvoyRateLimitAuthType,
isRouteRateLimitedByEnvoy,
} from "@/modules/core/rate-limit/envoy-rate-limit-coverage";
import { applyIPRateLimit, applyRateLimit } from "@/modules/core/rate-limit/helpers";
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
import { TRateLimitConfig } from "@/modules/core/rate-limit/types/rate-limit";
@@ -61,29 +65,58 @@ const applyClientRateLimit = async (customRateLimitConfig?: TRateLimitConfig): P
await applyIPRateLimit(customRateLimitConfig ?? rateLimitConfigs.api.client);
};
const getEnvoyRateLimitAuthType = (
authentication: TApiV1Authentication
): TEnvoyRateLimitAuthType | "unknown" => {
if (!authentication) {
return "none";
}
if ("user" in authentication) {
return "session";
}
if ("apiKeyId" in authentication) {
return "apiKey";
}
return "unknown";
};
/**
* Handle rate limiting based on authentication and API type
*/
const handleRateLimiting = async (
req: NextRequest,
authentication: TApiV1Authentication,
routeType: ApiV1RouteTypeEnum,
customRateLimitConfig?: TRateLimitConfig
): Promise<Response | null> => {
const authType = getEnvoyRateLimitAuthType(authentication);
if (authType === "unknown") {
logger.error({ authentication }, "Unknown authentication type");
return responses.internalServerErrorResponse("Invalid authentication configuration");
}
const isEnvoyManagedRateLimit = isRouteRateLimitedByEnvoy({
pathname: req.nextUrl.pathname,
method: req.method,
authType,
});
try {
if (authentication) {
if (authentication && !isEnvoyManagedRateLimit) {
if ("user" in authentication) {
// Session-based authentication for integration routes
await applyRateLimit(customRateLimitConfig ?? rateLimitConfigs.api.v1, authentication.user.id);
} else if ("apiKeyId" in authentication) {
// API key authentication for general routes
await applyRateLimit(customRateLimitConfig ?? rateLimitConfigs.api.v1, authentication.apiKeyId);
} else {
logger.error({ authentication }, "Unknown authentication type");
return responses.internalServerErrorResponse("Invalid authentication configuration");
}
}
if (routeType === ApiV1RouteTypeEnum.Client) {
if (routeType === ApiV1RouteTypeEnum.Client && !isEnvoyManagedRateLimit) {
await applyClientRateLimit(customRateLimitConfig);
}
} catch (error) {
@@ -286,7 +319,12 @@ export const withV1ApiWrapper = <TResult extends { response: Response; error?: u
// === Rate Limiting ===
if (isRateLimited) {
const rateLimitResponse = await handleRateLimiting(authentication, routeType, customRateLimitConfig);
const rateLimitResponse = await handleRateLimiting(
req,
authentication,
routeType,
customRateLimitConfig
);
if (rateLimitResponse) return rateLimitResponse;
}
@@ -121,13 +121,10 @@ export const DELETE = async (
: responses.notAuthenticatedResponse();
}
if (authResult.ok) {
// Rate limiting for apiKey DELETE is enforced by Envoy in v5 — see envoy-rate-limit-coverage.ts
if (authResult.ok && authResult.data.authType !== "apiKey") {
try {
if (authResult.data.authType === "apiKey") {
await applyRateLimit(rateLimitConfigs.storage.delete, authResult.data.apiKeyId);
} else {
await applyRateLimit(rateLimitConfigs.storage.delete, authResult.data.userId);
}
await applyRateLimit(rateLimitConfigs.storage.delete, authResult.data.userId);
} catch (error) {
return responses.tooManyRequestsResponse(
error instanceof Error ? error.message : "Unknown error occurred"
+17 -32
View File
@@ -6,7 +6,6 @@ import { EMAIL_VERIFICATION_DISABLED } from "@/lib/constants";
import { capturePostHogEvent } from "@/lib/posthog";
// Import mocked rate limiting functions
import { applyIPRateLimit } from "@/modules/core/rate-limit/helpers";
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
import { authOptions } from "./authOptions";
import { mockUser } from "./mock-data";
import { hashPassword } from "./utils";
@@ -220,8 +219,8 @@ describe("authOptions", () => {
});
}, 15000);
describe("Rate Limiting", () => {
test("should apply rate limiting before credential validation", async () => {
describe("Envoy-managed callback behavior", () => {
test("should not apply in-app rate limiting before credential validation", async () => {
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
vi.spyOn(prisma.user, "findUnique").mockResolvedValue({
id: mockUserId,
@@ -235,27 +234,14 @@ describe("authOptions", () => {
await credentialsProvider.options.authorize(credentials, {});
expect(applyIPRateLimit).toHaveBeenCalledWith(rateLimitConfigs.auth.login);
expect(applyIPRateLimit).toHaveBeenCalledBefore(prisma.user.findUnique as any);
expect(applyIPRateLimit).not.toHaveBeenCalled();
expect(prisma.user.findUnique).toHaveBeenCalled();
});
test("should block login when rate limit exceeded", async () => {
test("should ignore app limiter errors because login is Envoy-managed", async () => {
vi.mocked(applyIPRateLimit).mockRejectedValue(
new Error("Maximum number of requests reached. Please try again later.")
);
const findUniqueSpy = vi.spyOn(prisma.user, "findUnique");
const credentials = { email: mockUser.email, password: mockPassword };
await expect(credentialsProvider.options.authorize(credentials, {})).rejects.toThrow(
"Maximum number of requests reached. Please try again later."
);
expect(findUniqueSpy).not.toHaveBeenCalled();
});
test("should use correct rate limit configuration", async () => {
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
vi.spyOn(prisma.user, "findUnique").mockResolvedValue({
id: mockUserId,
email: mockUser.email,
@@ -266,13 +252,14 @@ describe("authOptions", () => {
const credentials = { email: mockUser.email, password: mockPassword };
await credentialsProvider.options.authorize(credentials, {});
const result = await credentialsProvider.options.authorize(credentials, {});
expect(applyIPRateLimit).toHaveBeenCalledWith({
interval: 900,
allowedPerInterval: 30,
namespace: "auth:login",
expect(result).toEqual({
id: mockUserId,
email: mockUser.email,
emailVerified: expect.any(Date),
});
expect(applyIPRateLimit).not.toHaveBeenCalled();
});
});
@@ -315,30 +302,28 @@ describe("authOptions", () => {
);
});
describe("Rate Limiting", () => {
test("should apply rate limiting before token verification", async () => {
describe("Envoy-managed callback behavior", () => {
test("should not apply in-app rate limiting before token verification", async () => {
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
const credentials = { token: "sometoken" };
await expect(tokenProvider.options.authorize(credentials, {})).rejects.toThrow();
expect(applyIPRateLimit).toHaveBeenCalledWith(rateLimitConfigs.auth.verifyEmail);
expect(applyIPRateLimit).not.toHaveBeenCalled();
});
test("should block verification when rate limit exceeded", async () => {
test("should ignore app limiter errors because token verification is Envoy-managed", async () => {
vi.mocked(applyIPRateLimit).mockRejectedValue(
new Error("Maximum number of requests reached. Please try again later.")
);
const findUniqueSpy = vi.spyOn(prisma.user, "findUnique");
const credentials = { token: "sometoken" };
await expect(tokenProvider.options.authorize(credentials, {})).rejects.toThrow(
"Maximum number of requests reached. Please try again later."
"Either a user does not match the provided token or the token is invalid"
);
expect(findUniqueSpy).not.toHaveBeenCalled();
expect(applyIPRateLimit).not.toHaveBeenCalled();
});
});
});
-6
View File
@@ -29,8 +29,6 @@ import {
shouldLogAuthFailure,
verifyPassword,
} from "@/modules/auth/lib/utils";
import { applyIPRateLimit } from "@/modules/core/rate-limit/helpers";
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
import { UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
import { getSSOProviders } from "@/modules/ee/sso/lib/providers";
import { handleSsoCallback } from "@/modules/ee/sso/lib/sso-handlers";
@@ -62,8 +60,6 @@ export const authOptions: NextAuthOptions = {
backupCode: { label: "Backup Code", type: "input", placeholder: "Two-factor backup code" },
},
async authorize(credentials, _req) {
await applyIPRateLimit(rateLimitConfigs.auth.login);
// Use email for rate limiting when available, fall back to "unknown_user" for credential validation
const identifier = credentials?.email || "unknown_user"; // NOSONAR // We want to check for empty strings
@@ -252,8 +248,6 @@ export const authOptions: NextAuthOptions = {
},
},
async authorize(credentials, _req) {
await applyIPRateLimit(rateLimitConfigs.auth.verifyEmail);
// For token verification, we can't rate limit effectively by token (single-use)
// So we use a generic identifier for token abuse attempts
const identifier = "email_verification_attempts";
@@ -0,0 +1,246 @@
import { describe, expect, test } from "vitest";
import { isRouteRateLimitedByEnvoy } from "./envoy-rate-limit-coverage";
describe("isRouteRateLimitedByEnvoy", () => {
test("matches covered auth callback routes", () => {
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/auth/callback/credentials",
method: "POST",
authType: "none",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/auth/callback/token",
method: "POST",
authType: "none",
})
).toBe(true);
});
test("matches covered api-key management and webhook routes", () => {
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/management/surveys",
method: "GET",
authType: "apiKey",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/management/storage",
method: "POST",
authType: "apiKey",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/webhooks/webhook-id",
method: "DELETE",
authType: "apiKey",
})
).toBe(true);
});
test("matches covered client routes", () => {
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/client/env_123/environment",
method: "GET",
authType: "none",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/client/env_123/responses",
method: "POST",
authType: "none",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/client/env_123/responses/response_123",
method: "PUT",
authType: "none",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/client/env_123/displays",
method: "POST",
authType: "none",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/client/env_123/user",
method: "POST",
authType: "none",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/client/env_123/storage",
method: "POST",
authType: "none",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v2/client/env_123/responses",
method: "POST",
authType: "none",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v2/client/env_123/responses/response_123",
method: "PUT",
authType: "none",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v2/client/env_123/displays",
method: "POST",
authType: "none",
})
).toBe(true);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v2/client/env_123/storage",
method: "POST",
authType: "none",
})
).toBe(true);
});
test("matches covered api-key storage delete route", () => {
expect(
isRouteRateLimitedByEnvoy({
pathname: "/storage/env_123/private/file.pdf",
method: "DELETE",
authType: "apiKey",
})
).toBe(true);
});
test("does not match excluded or uncovered routes", () => {
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/client/og",
method: "GET",
authType: "none",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v2/health",
method: "GET",
authType: "none",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/client/env_123/environment",
method: "PATCH",
authType: "none",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/client/env_123/displays",
method: "GET",
authType: "none",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v2/client/env_123/responses",
method: "PUT",
authType: "none",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v2/client/env_123/responses/response_123",
method: "POST",
authType: "none",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v2/client/env_123/environment",
method: "GET",
authType: "none",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v2/client/env_123/user",
method: "POST",
authType: "none",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/management/me",
method: "GET",
authType: "session",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/management/storage",
method: "POST",
authType: "session",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/storage/env_123/private/file.pdf",
method: "DELETE",
authType: "session",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/webhooks",
method: "GET",
authType: "apiKey",
})
).toBe(false);
expect(
isRouteRateLimitedByEnvoy({
pathname: "/api/v1/client/env_123/environment",
method: "OPTIONS",
authType: "none",
})
).toBe(false);
});
});
@@ -0,0 +1,117 @@
export type TEnvoyRateLimitAuthType = "none" | "apiKey" | "session";
type TEnvoyRateLimitRequest = {
pathname: string;
method: string;
authType: TEnvoyRateLimitAuthType;
};
const V1_CLIENT_STORAGE_PATTERN = /^\/api\/v1\/client\/[^/]+\/storage$/;
const V1_CLIENT_ENVIRONMENT_PATTERN = /^\/api\/v1\/client\/[^/]+\/environment$/;
const V1_CLIENT_RESPONSES_PATTERN = /^\/api\/v1\/client\/[^/]+\/responses$/;
const V1_CLIENT_RESPONSE_PATTERN = /^\/api\/v1\/client\/[^/]+\/responses\/[^/]+$/;
const V1_CLIENT_DISPLAYS_PATTERN = /^\/api\/v1\/client\/[^/]+\/displays$/;
const V1_CLIENT_USER_PATTERN = /^\/api\/v1\/client\/[^/]+\/user$/;
const V2_CLIENT_RESPONSES_PATTERN = /^\/api\/v2\/client\/[^/]+\/responses$/;
const V2_CLIENT_RESPONSE_PATTERN = /^\/api\/v2\/client\/[^/]+\/responses\/[^/]+$/;
const V2_CLIENT_DISPLAYS_PATTERN = /^\/api\/v2\/client\/[^/]+\/displays$/;
const V2_CLIENT_STORAGE_PATTERN = /^\/api\/v2\/client\/[^/]+\/storage$/;
const STORAGE_DELETE_PATTERN = /^\/storage\/[^/]+\/(public|private)\/.+$/;
const V1_MANAGEMENT_PREFIX = "/api/v1/management/";
const V1_WEBHOOKS_PREFIX = "/api/v1/webhooks/";
const V1_GENERAL_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
const normalizeMethod = (method: string): string => method.toUpperCase();
const matchesPrefixedPath = (pathname: string, prefix: string): boolean => pathname.startsWith(prefix);
/**
* Mirrors the live Envoy rate-limit policy set.
* Keep this matcher aligned with the Gateway policies when coverage changes.
*/
export const isRouteRateLimitedByEnvoy = ({
pathname,
method,
authType,
}: TEnvoyRateLimitRequest): boolean => {
const normalizedMethod = normalizeMethod(method);
if (normalizedMethod === "OPTIONS") {
return false;
}
if (authType === "none" && normalizedMethod === "POST" && pathname === "/api/auth/callback/credentials") {
return true;
}
if (authType === "none" && normalizedMethod === "POST" && pathname === "/api/auth/callback/token") {
return true;
}
if (
authType === "apiKey" &&
V1_GENERAL_METHODS.has(normalizedMethod) &&
matchesPrefixedPath(pathname, V1_MANAGEMENT_PREFIX)
) {
return true;
}
if (
authType === "apiKey" &&
V1_GENERAL_METHODS.has(normalizedMethod) &&
matchesPrefixedPath(pathname, V1_WEBHOOKS_PREFIX)
) {
return true;
}
if (authType === "apiKey" && normalizedMethod === "DELETE" && STORAGE_DELETE_PATTERN.test(pathname)) {
return true;
}
if (authType !== "none") {
return false;
}
if (normalizedMethod === "POST" && V1_CLIENT_STORAGE_PATTERN.test(pathname)) {
return true;
}
if (normalizedMethod === "GET" && V1_CLIENT_ENVIRONMENT_PATTERN.test(pathname)) {
return true;
}
if (normalizedMethod === "POST" && V1_CLIENT_RESPONSES_PATTERN.test(pathname)) {
return true;
}
if (normalizedMethod === "PUT" && V1_CLIENT_RESPONSE_PATTERN.test(pathname)) {
return true;
}
if (normalizedMethod === "POST" && V1_CLIENT_DISPLAYS_PATTERN.test(pathname)) {
return true;
}
if (normalizedMethod === "POST" && V1_CLIENT_USER_PATTERN.test(pathname)) {
return true;
}
if (normalizedMethod === "POST" && V2_CLIENT_RESPONSES_PATTERN.test(pathname)) {
return true;
}
if (normalizedMethod === "PUT" && V2_CLIENT_RESPONSE_PATTERN.test(pathname)) {
return true;
}
if (normalizedMethod === "POST" && V2_CLIENT_DISPLAYS_PATTERN.test(pathname)) {
return true;
}
if (normalizedMethod === "POST" && V2_CLIENT_STORAGE_PATTERN.test(pathname)) {
return true;
}
return false;
};
@@ -0,0 +1,312 @@
# Environment Reference Audit
Inventory of references to `environmentId` and `environment`/`environments` across product and docs code.
## Scope
- `apps/web`
- `packages`
- `docs`
- `apps/storybook`
- `openapi.yml`
## Totals
- Files with matches: **290**
- Total `environmentId` matches: **994**
- Total `environment`/`environments` matches: **997**
## File-Level Inventory
| Area | File | environmentId Count | environment/environments Count | Suggested Handling |
| --- | --- | ---: | ---: | --- |
| Product | `apps/storybook/src/stories/Configure.mdx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/lib/surveys.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/app/(app)/workspaces/[workspaceId]/components/WorkspaceStorageHandler.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/utils.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/(redirects)/environments/[environmentId]/[...path]/route.ts` | 4 | 0 | Keep route for backward compatibility, but label as legacy/deprecated and redirect to workspace URL shape. |
| Product | `apps/web/app/(redirects)/environments/[environmentId]/route.ts` | 4 | 0 | Keep route for backward compatibility, but label as legacy/deprecated and redirect to workspace URL shape. |
| Product | `apps/web/app/api/(internal)/pipeline/lib/telemetry.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/api/v1/client/[workspaceId]/displays/route.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.ts` | 0 | 4 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/api/v1/client/[workspaceId]/environment/lib/environmentState.test.ts` | 1 | 1 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
| Product | `apps/web/app/api/v1/client/[workspaceId]/environment/lib/environmentState.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/api/v1/client/[workspaceId]/environment/route.ts` | 1 | 4 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v1/client/[workspaceId]/responses/route.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v1/client/[workspaceId]/storage/route.ts` | 1 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts` | 1 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v1/management/lib/workspace-resolver.ts` | 3 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v1/management/me/route.ts` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/api/v1/management/responses/route.ts` | 1 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v1/management/surveys/route.ts` | 1 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v1/webhooks/route.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v2/client/[environmentId]/environment/route.test.ts` | 0 | 9 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/app/api/v2/client/[workspaceId]/displays/route.test.ts` | 8 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
| Product | `apps/web/app/api/v2/client/[workspaceId]/displays/route.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v2/client/[workspaceId]/environment/route.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/api/v2/client/[workspaceId]/responses/route.test.ts` | 7 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
| Product | `apps/web/app/api/v2/client/[workspaceId]/responses/route.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/api/v3/lib/workspace-context.test.ts` | 1 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
| Product | `apps/web/app/api/v3/lib/workspace-context.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/app/lib/api/api-backwards-compat.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/lib/api/api-error-reporter.test.ts` | 0 | 4 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/app/lib/api/parse-and-validate-json-body.test.ts` | 3 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
| Product | `apps/web/app/lib/api/with-api-logging.test.ts` | 0 | 4 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/app/middleware/domain-utils.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/middleware/endpoint-validator.test.ts` | 0 | 18 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/app/middleware/endpoint-validator.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/middleware/route-config.ts` | 1 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `apps/web/app/sentry/SentryProvider.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/app/storage/[workspaceId]/[accessType]/[fileName]/route.ts` | 3 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `apps/web/i18n.lock` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/instrumentation-node.ts` | 0 | 9 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/lib/actionClass/service.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/lib/cache/index.test.ts` | 0 | 2 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/lib/constants.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/lib/env.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/lib/env.ts` | 0 | 5 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/lib/integration/service.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/lib/jwt.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/lib/localStorage.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/lib/organization/service.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/lib/survey/service.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/lib/utils/resolve-client-id.ts` | 1 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `apps/web/lib/utils/services.ts` | 1 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `apps/web/locales/de-DE.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/en-US.json` | 1 | 12 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/es-ES.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/fr-FR.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/hu-HU.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/ja-JP.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/nl-NL.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/pt-BR.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/pt-PT.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/ro-RO.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/ru-RU.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/sv-SE.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/zh-Hans-CN.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/locales/zh-Hant-TW.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
| Product | `apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/api/v2/management/lib/workspace-resolver.ts` | 3 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/tests/contact.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/api/v2/management/surveys/types/surveys.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/modules/auth/lib/verification-links.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/core/rate-limit/envoy-rate-limit-coverage.test.ts` | 0 | 4 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/core/rate-limit/envoy-rate-limit-coverage.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/core/rate-limit/rate-limit-load.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/core/rate-limit/rate-limit.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/ee/contacts/api/v1/management/contact-attribute-keys/route.ts` | 1 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
| Product | `apps/web/modules/ee/contacts/api/v2/management/contacts/bulk/lib/openapi.ts` | 2 | 0 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
| Product | `apps/web/modules/ee/contacts/api/v2/management/contacts/bulk/route.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/ee/contacts/api/v2/management/contacts/lib/openapi.ts` | 2 | 3 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
| Product | `apps/web/modules/ee/contacts/api/v2/management/contacts/route.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/ee/contacts/lib/attributes.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/ee/license-check/lib/license.test.ts` | 0 | 16 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/ee/license-check/lib/license.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/ee/sso/lib/providers.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/organization/settings/api-keys/lib/api-keys.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/storage/service.test.ts` | 1 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
| Product | `apps/web/modules/storage/service.ts` | 3 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `apps/web/modules/survey/components/template-list/lib/survey.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/survey/editor/components/edit-welcome-card.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/survey/editor/components/survey-editor.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/survey/editor/lib/action-utils.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/survey/editor/lib/workspace.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/survey/link/contact-survey/page.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/survey/link/page.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/survey/list/types/surveys.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/survey/templates/components/template-container.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/ui/components/delete-dialog/stories.tsx` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/ui/components/preview-survey/index.tsx` | 0 | 5 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/utils/hooks/useGetBillingInfo.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/modules/workspaces/lib/utils.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/workspaces/settings/(setup)/app-connection/loading.tsx` | 1 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `apps/web/modules/workspaces/settings/(setup)/app-connection/page.tsx` | 1 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `apps/web/modules/workspaces/settings/(setup)/components/ActionActivityTab.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/modules/workspaces/settings/general/actions.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/next.config.mjs` | 6 | 7 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `apps/web/playwright/api/auth/security.spec.ts` | 0 | 6 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/playwright/js.spec.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/playwright/lib/utils.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/playwright/survey.spec.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/proxy.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `apps/web/scripts/docker/read-secrets.sh` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/scripts/openapi/merge-client-endpoints.ts` | 10 | 10 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
| Product | `apps/web/sentry.edge.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/sentry.server.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/tsconfig.tsbuildinfo` | 483 | 151 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `apps/web/vite.config.mts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `apps/web/vitestSetup.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Docs | `docs/api-reference/client-api--display/create-display.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/api-reference/client-api--display/update-display.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/api-reference/client-api--people/create-person.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/api-reference/client-api--people/update-person.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/api-reference/client-api--response/create-response.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/api-reference/client-api--response/update-response.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/api-reference/generate-key.mdx` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/api-reference/openapi.json` | 85 | 36 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
| Docs | `docs/api-v2-reference/introduction.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/api-v2-reference/openapi.yml` | 52 | 35 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
| Docs | `docs/api-v3-reference/openapi.yml` | 1 | 8 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
| Docs | `docs/development/contribution/contribution.mdx` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/development/local-setup/github-codespaces.mdx` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/development/standards/organization/file-and-directory-organization.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/development/standards/organization/naming-conventions.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/development/standards/practices/error-handling.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/development/standards/technical/language-specific-conventions.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/development/technical-handbook/background-job-processing.mdx` | 3 | 1 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/development/technical-handbook/database-model.mdx` | 0 | 14 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/development/technical-handbook/tenant-separation.mdx` | 0 | 14 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/docs.json` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/overview/open-source.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/advanced/enterprise-features/audit-logging.mdx` | 1 | 1 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/self-hosting/advanced/license-activation.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/advanced/migration.mdx` | 3 | 38 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/self-hosting/advanced/rate-limiting.mdx` | 15 | 2 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/self-hosting/auth-behavior.mdx` | 0 | 6 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/auth-sso/azure-ad-oauth.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/auth-sso/google-oauth.mdx` | 0 | 5 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/auth-sso/keycloak-oidc.mdx` | 0 | 4 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/auth-sso/open-id-connect.mdx` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/auth-sso/saml-sso.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/cdn.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/self-hosting/configuration/custom-ssl.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/custom-subpath.mdx` | 0 | 4 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/domain-configuration.mdx` | 5 | 2 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/self-hosting/configuration/environment-variables.mdx` | 0 | 3 | No entity rename: this is deployment env-var documentation, not the deprecated product Environment entity. |
| Docs | `docs/self-hosting/configuration/file-uploads.mdx` | 0 | 5 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/integrations/airtable.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/integrations/google-sheets.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/integrations/n8n.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/integrations/notion.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/integrations/slack.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/configuration/smtp.mdx` | 0 | 6 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/setup/cluster-setup.mdx` | 0 | 4 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/setup/docker.mdx` | 0 | 6 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/setup/kubernetes.mdx` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/setup/monitoring.mdx` | 0 | 7 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/self-hosting/setup/one-click.mdx` | 0 | 8 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/core-features/integrations/n8n.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/core-features/integrations/notion.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/core-features/integrations/slack.mdx` | 0 | 4 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/core-features/integrations/webhooks.mdx` | 0 | 5 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/core-features/integrations/wordpress.mdx` | 4 | 2 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/xm-and-surveys/core-features/integrations/zapier.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/core-features/test-environment.mdx` | 0 | 19 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/surveys/general-features/multi-language-surveys.mdx` | 1 | 1 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/xm-and-surveys/surveys/general-features/recall.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/surveys/general-features/spam-protection.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/surveys/general-features/tags.mdx` | 0 | 6 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/surveys/website-app-surveys/actions.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/surveys/website-app-surveys/framework-guides.mdx` | 9 | 24 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/xm-and-surveys/surveys/website-app-surveys/google-tag-manager.mdx` | 3 | 3 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Docs | `docs/xm-and-surveys/surveys/website-app-surveys/quickstart.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/surveys/website-app-surveys/user-identification.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/xm/best-practices/docs-feedback.mdx` | 0 | 10 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
| Docs | `docs/xm-and-surveys/xm/best-practices/headless-surveys.mdx` | 3 | 6 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
| Product | `openapi.yml` | 4 | 0 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
| Product | `packages/ai/src/provider.test.ts` | 0 | 5 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/ai/src/provider.ts` | 0 | 15 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/ai/src/providers/aws.ts` | 0 | 11 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/ai/src/providers/azure.ts` | 0 | 13 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/ai/src/providers/gcp.ts` | 0 | 15 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/ai/src/registry.ts` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/ai/src/shared.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/ai/src/text.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/ai/src/text.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/ai/vite.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/cache/.cursor/rules/cache-package.md` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/cache/src/cache-integration.test.ts` | 0 | 2 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/cache/src/cache-keys.test.ts` | 0 | 6 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/cache/src/client.test.ts` | 0 | 2 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/cache/src/client.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/cache/types/error.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/cache/vite.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/database/README.md` | 0 | 5 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/database/migration/20230329205933_init/migration.sql` | 10 | 6 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20230405105937_add_api_keys_to_environments/migration.sql` | 3 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20230505085230_add_webhooks/migration.sql` | 2 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20230624161355_add_tags/migration.sql` | 3 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20230915154251_add_integration/migration.sql` | 3 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20231030105533_add_cascade_delete_to_integrations/migration.sql` | 1 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20231107145619_add_indexes/migration.sql` | 7 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20231109052945_restructure_session_action_person/migration.sql` | 2 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20240207041922_advanced_targeting/migration.sql` | 4 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20240229141200_add_attribute_class_indexes/migration.sql` | 2 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20240327140901_add_more_indexes/migration.sql` | 2 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20240501111944_refactors_actions_and_removes_inline_triggers/migration.sql` | 2 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20240610055828_adds_app_and_website_status_indicators/migration.sql` | 0 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20241004070040_removed_website_setup_completed/migration.sql` | 0 | 2 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20241010133706_xm_user_identification/migration.sql` | 3 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20241017124431_add_documents_and_insights/migration.sql` | 4 | 2 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20241120150728_product_revamp/migration.sql` | 0 | 5 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20241209104738_xm_user_identification/migration.ts` | 11 | 12 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20241209111404_xm_attribute_removal/migration.ts` | 11 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20250326083401_add_api_keys_to_organization/migration.sql` | 4 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20250326111101_move_api_keys_to_api_keys_new/migration.ts` | 7 | 4 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20250911192630_remove_deprecated_fields_and_tables/migration.sql` | 0 | 2 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20260204124556_add_language_default_attribute_key/migration.ts` | 2 | 4 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20260330000000_rename_project_to_workspace/migration.sql` | 0 | 3 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20260401000000_add_workspace_id_to_environment_owned_models/migration.sql` | 0 | 3 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20260401000001_promote_dev_environments/migration.ts` | 10 | 21 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20260401000002_backfill_workspace_id/migration.ts` | 2 | 2 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20260402000000_make_workspace_id_not_null/migration.sql` | 8 | 2 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migration/20260403000000_remove_environment_model/migration.sql` | 12 | 6 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
| Product | `packages/database/migrations/20230329205933_init/migration.sql` | 10 | 6 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20230405105937_add_api_keys_to_environments/migration.sql` | 3 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20230505085230_add_webhooks/migration.sql` | 2 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20230624161355_add_tags/migration.sql` | 3 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20230915154251_add_integration/migration.sql` | 3 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20231030105533_add_cascade_delete_to_integrations/migration.sql` | 1 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20231107145619_add_indexes/migration.sql` | 7 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20231109052945_restructure_session_action_person/migration.sql` | 2 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20240207041922_advanced_targeting/migration.sql` | 4 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20240229141200_add_attribute_class_indexes/migration.sql` | 2 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20240327140901_add_more_indexes/migration.sql` | 2 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20240501111944_refactors_actions_and_removes_inline_triggers/migration.sql` | 2 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20240610055828_adds_app_and_website_status_indicators/migration.sql` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/database/migrations/20241004070040_removed_website_setup_completed/migration.sql` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/database/migrations/20241010133706_xm_user_identification/migration.sql` | 3 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20241017124431_add_documents_and_insights/migration.sql` | 4 | 2 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20241120150728_product_revamp/migration.sql` | 0 | 5 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/database/migrations/20250326083401_add_api_keys_to_organization/migration.sql` | 4 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20250911192630_remove_deprecated_fields_and_tables/migration.sql` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/database/migrations/20260330000000_rename_project_to_workspace/migration.sql` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/database/migrations/20260401000000_add_workspace_id_to_environment_owned_models/migration.sql` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/database/migrations/20260402000000_make_workspace_id_not_null/migration.sql` | 8 | 2 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/database/migrations/20260403000000_remove_environment_model/migration.sql` | 12 | 6 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/email/src/lib/mock-translate.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/i18n-utils/vite.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/js-core/README.md` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/js-core/src/index.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/js-core/src/lib/common/api.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/js-core/src/lib/common/setup.ts` | 9 | 7 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/js-core/src/lib/common/tests/api.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/js-core/src/lib/common/tests/setup.test.ts` | 18 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
| Product | `packages/js-core/src/lib/common/tests/utils.test.ts` | 0 | 2 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/js-core/src/lib/workspace/state.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/js-core/src/lib/workspace/tests/state.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/js-core/src/types/config.ts` | 3 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
| Product | `packages/js-core/vite.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/logger/src/logger.test.ts` | 0 | 4 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/logger/src/logger.ts` | 0 | 4 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/storage/.cursor/rules/storage-package.md` | 0 | 9 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/storage/src/client.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/storage/src/client.ts` | 0 | 4 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/storage/src/constants.test.ts` | 0 | 7 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/storage/src/service.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/storage/vite.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/survey-ui/vite.config.mts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/surveys/README.md` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/surveys/src/lib/html-utils.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/surveys/src/lib/html-utils.ts` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/surveys/src/lib/response.queue.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/surveys/src/lib/styles.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/surveys/src/lib/ttc.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/surveys/src/lib/use-online-status.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
| Product | `packages/surveys/vite.config.mts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
| Product | `packages/types/common.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
+24
View File
@@ -4,6 +4,23 @@ description: "Formbricks Self-hosted version migration"
icon: "arrow-right"
---
## v5
Formbricks v5 changes how rate limiting is enforced:
- several public and API-key routes are no longer rate-limited inside the application server
- those routes are now expected to be protected by Envoy Gateway or an equivalent edge rate limiter
- the remaining session-based routes, server actions, and uncovered APIs still use the in-app limiter
<Warning>
If you self-host Formbricks without Envoy or another equivalent edge rate limiter, upgrade planning for v5
must include new edge protection for the covered routes. Otherwise those routes will no longer be throttled
by the application server after the upgrade.
</Warning>
See the [rate-limiting guide](/self-hosting/advanced/rate-limiting) for the exact covered route groups, thresholds,
and the remaining app-enforced limits.
## v4.7
Formbricks v4.7 introduces **typed contact attributes** with native `number` and `date` data types. This enables comparison-based segment filters (e.g. "signup date before 2025-01-01") that were previously not possible with string-only attribute values.
@@ -39,6 +56,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
If you run into "**No such container**", use `docker ps` to find your container name, e.g.
`formbricks_postgres_1`.
</Info>
</Tab>
<Tab title="Kubernetes">
If you are using the **in-cluster PostgreSQL** deployed by the Helm chart:
@@ -52,6 +70,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
</Info>
If you are using a **managed PostgreSQL** service (e.g. AWS RDS, Cloud SQL), use your provider's backup/snapshot feature or run `pg_dump` directly against the external host.
</Tab>
</Tabs>
@@ -69,6 +88,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
# Start with Formbricks v4.7
docker compose up -d
```
</Tab>
<Tab title="Kubernetes">
```bash
@@ -81,6 +101,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
The Helm chart includes a migration Job that automatically runs Prisma schema migrations as a
PreSync hook before the new pods start. No manual migration step is needed.
</Info>
</Tab>
</Tabs>
@@ -105,6 +126,7 @@ After Formbricks starts, check the logs to see whether the value backfill was co
```bash
kubectl logs -n formbricks job/formbricks-migration
```
</Tab>
</Tabs>
@@ -121,6 +143,7 @@ If the migration skipped the value backfill, run the standalone backfill script
```
<Info>Replace `formbricks` with your actual container name if it differs. Use `docker ps` to find it.</Info>
</Tab>
<Tab title="Kubernetes">
```bash
@@ -130,6 +153,7 @@ If the migration skipped the value backfill, run the standalone backfill script
<Info>
If your Formbricks deployment has a different name, run `kubectl get deploy -n formbricks` to find it.
</Info>
</Tab>
</Tabs>
+84 -37
View File
@@ -6,65 +6,106 @@ icon: "timer"
Formbricks applies request rate limits to protect against abuse and keep API usage fair.
Rate limits are scoped by identifier, depending on the endpoint:
Starting with Formbricks v5, rate limiting is split across two layers:
- Envoy Gateway for public and API-key routes that can be enforced at ingress
- The application server for remaining session-authenticated routes, server actions, and other flows Envoy does
not currently cover
<Warning>
Formbricks v5 removes application-level rate limiting for several routes that are now expected to be
protected by Envoy Gateway. If you self-host Formbricks without Envoy or an equivalent edge rate limiter,
those routes will no longer be throttled by the application server after upgrading.
</Warning>
Rate limits are scoped by identifier, depending on the endpoint and enforcement layer:
- IP hash (for unauthenticated/client-side routes and public actions)
- API key ID (for authenticated API calls)
- API key ID (for Envoy-managed and app-managed authenticated API calls)
- User ID (for authenticated session-based calls and server actions)
- Organization ID (for follow-up email dispatch)
When a limit is exceeded, the API returns `429 Too Many Requests`.
## Management API Rate Limits
## v5 Migration Note
These are the current limits for Management APIs:
Before upgrading to Formbricks v5:
| **Route Group** | **Limit** | **Window** | **Identifier** |
| --- | --- | --- | --- |
| `/api/v1/management/*` (except `/api/v1/management/storage`), `/api/v1/webhooks/*`, `/api/v1/integrations/*`, `/api/v1/management/me` | 100 requests | 1 minute | API key ID or session user ID |
| `/api/v2/management/*` (and other v2 authenticated routes that use `authenticatedApiClient`) | 100 requests | 1 minute | API key ID |
| `POST /api/v1/management/storage` | 5 requests | 1 minute | API key ID or session user ID |
- deploy Envoy Gateway or an equivalent edge rate limiter for the covered routes below
- keep application Redis/Valkey enabled for the remaining app-enforced limits
- expect covered routes to emit gateway `429`s instead of the legacy app JSON `429`s
## All Enforced Limits
For the current source of truth on covered routes and thresholds, use this page together with your deployment
configuration.
| **Config** | **Limit** | **Window** | **Identifier** | **Used For** |
| --- | --- | --- | --- | --- |
| `auth.login` | 10 requests | 15 minutes | IP hash | Email/password login flow (`/api/auth/callback/credentials`) |
| `auth.signup` | 30 requests | 60 minutes | IP hash | Signup server action |
| `auth.forgotPassword` | 5 requests | 60 minutes | IP hash | Forgot password server action |
| `auth.verifyEmail` | 10 requests | 60 minutes | IP hash | Email verification callback + resend verification action |
| `api.v1` | 100 requests | 1 minute | API key ID or session user ID | v1 management, webhooks, integrations, and `/api/v1/management/me` |
| `api.v2` | 100 requests | 1 minute | API key ID | v2 authenticated API wrapper (`authenticatedApiClient`) |
| `api.client` | 100 requests | 1 minute | IP hash | v1 client API routes (except `/api/v1/client/og` and storage upload override), plus v2 routes that re-use those v1 handlers |
| `storage.upload` | 5 requests | 1 minute | IP hash or authenticated ID | Client storage upload and management storage upload |
| `storage.delete` | 5 requests | 1 minute | API key ID or session user ID | `DELETE /storage/[environmentId]/[accessType]/[fileName]` |
| `actions.emailUpdate` | 3 requests | 60 minutes | User ID | Profile email update action |
| `actions.surveyFollowUp` | 50 requests | 60 minutes | Organization ID | Survey follow-up email processing |
| `actions.sendLinkSurveyEmail` | 10 requests | 60 minutes | IP hash | Link survey email send action |
| `actions.licenseRecheck` | 5 requests | 1 minute | User ID | Enterprise license recheck action |
## Envoy-Managed Limits
## Current Endpoint Exceptions
These limits are expected to be enforced at the gateway layer in Formbricks v5 and later:
The following routes are currently not rate-limited by the server-side limiter:
| **Route Group** | **Limit** | **Window** | **Identifier** |
| ------------------------------------------------------------------------ | ------------ | ---------- | -------------- |
| `POST /api/auth/callback/credentials` | 40 requests | 1 hour | IP hash |
| `POST /api/auth/callback/token` | 10 requests | 1 hour | IP hash |
| `GET, POST, PUT, PATCH, DELETE /api/v1/management/*` (API key auth only) | 100 requests | 1 minute | API key ID |
| `POST /api/v1/management/storage` (API key auth only) | 5 requests | 1 minute | API key ID |
| `GET, POST, PUT, PATCH, DELETE /api/v1/webhooks/*` (API key auth only) | 100 requests | 1 minute | API key ID |
| `GET /api/v1/client/[environmentId]/environment` | 100 requests | 1 minute | IP hash |
| `POST /api/v1/client/[environmentId]/responses` | 100 requests | 1 minute | IP hash |
| `PUT /api/v1/client/[environmentId]/responses/[responseId]` | 100 requests | 1 minute | IP hash |
| `POST /api/v1/client/[environmentId]/displays` | 100 requests | 1 minute | IP hash |
| `POST /api/v1/client/[environmentId]/user` | 100 requests | 1 minute | IP hash |
| `POST /api/v1/client/[environmentId]/storage` | 5 requests | 1 minute | IP hash |
| `POST /api/v2/client/[environmentId]/responses` | 100 requests | 1 minute | IP hash |
| `PUT /api/v2/client/[environmentId]/responses/[responseId]` | 100 requests | 1 minute | IP hash |
| `POST /api/v2/client/[environmentId]/displays` | 100 requests | 1 minute | IP hash |
| `POST /api/v2/client/[environmentId]/storage` | 5 requests | 1 minute | IP hash |
| `DELETE /storage/[environmentId]/public/[fileName]` (API key auth only) | 5 requests | 1 minute | API key ID |
| `DELETE /storage/[environmentId]/private/[fileName]` (API key auth only) | 5 requests | 1 minute | API key ID |
- `GET /api/v1/client/og` (explicitly excluded)
- `POST /api/v2/client/[environmentId]/responses`
- `POST /api/v2/client/[environmentId]/displays`
- `GET /api/v2/health`
Session-authenticated `/api/v1/management/*`, `/api/v1/management/me`, `/api/v1/management/storage`, and
`DELETE /storage/...` requests are **not** covered by the current Envoy policies and remain app-enforced.
## App-Enforced Limits
These are the limits that still run inside the Formbricks application server:
| **Config** | **Limit** | **Window** | **Identifier** | **Used For** |
| ----------------------------- | ------------ | ---------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth.signup` | 30 requests | 60 minutes | IP hash | Signup server action |
| `auth.forgotPassword` | 5 requests | 60 minutes | IP hash | Forgot password server action |
| `auth.verifyEmail` | 10 requests | 60 minutes | IP hash | Resend verification server action |
| `api.v1` | 100 requests | 1 minute | Session user ID | Session-authenticated v1 management routes, `/api/v1/management/me`, and `/api/v1/integrations/*` |
| `api.v2` | 100 requests | 1 minute | API key ID | Authenticated v2 API wrapper outside the Envoy-managed `/api/v2/client/*` subset |
| `api.v3` | 100 requests | 1 minute | API key ID or session user ID | v3 API wrapper |
| `api.client` | 100 requests | 1 minute | IP hash | Uncovered client routes such as `GET /api/v1/client/og`, `GET /api/v2/client/[environmentId]/environment`, and `POST /api/v2/client/[environmentId]/user` |
| `storage.upload` | 5 requests | 1 minute | Session user ID | Session-authenticated `POST /api/v1/management/storage` |
| `storage.delete` | 5 requests | 1 minute | Session user ID | Session-authenticated `DELETE /storage/[environmentId]/[accessType]/[fileName]` |
| `actions.emailUpdate` | 3 requests | 60 minutes | User ID | Profile email update action |
| `actions.surveyFollowUp` | 50 requests | 60 minutes | Organization ID | Survey follow-up email processing |
| `actions.sendLinkSurveyEmail` | 10 requests | 60 minutes | IP hash | Link survey email send action |
| `actions.licenseRecheck` | 5 requests | 1 minute | User ID | Enterprise license recheck action |
## Explicit Envoy Exclusions
The current Envoy policy set explicitly excludes these routes:
- `GET /api/v1/client/og` (still covered by the app-level `api.client` limiter)
- `GET /api/v2/health` (not rate-limited)
- `OPTIONS` requests (not rate-limited)
## 429 Response Shape
v1-style endpoints return:
Application-generated v1 `429`s return:
```json
{
"code": "too_many_requests",
"message": "Maximum number of requests reached. Please try again later.",
"details": {}
"details": {},
"message": "Maximum number of requests reached. Please try again later."
}
```
v2-style endpoints return:
Application-generated v2/v3 `429`s return:
```json
{
@@ -75,6 +116,9 @@ v2-style endpoints return:
}
```
Envoy-generated `429`s are gateway responses and should include an `x-envoy-ratelimited` header. Their exact body
shape is not the same stability contract as the in-app JSON responses above.
## Disabling Rate Limiting
For self-hosters, rate limiting can be disabled if necessary. We strongly recommend keeping it enabled in production.
@@ -87,8 +131,11 @@ RATE_LIMITING_DISABLED=1
After changing this value, restart the server.
This setting disables only the **application-level** limiter. It does **not** disable Envoy rate-limit policies.
## Operational Notes
- Redis/Valkey is required for robust rate limiting (`REDIS_URL`).
- If Redis is unavailable at runtime, rate-limiter checks currently fail open (requests are allowed through without enforcement).
- Redis/Valkey is required for the application-level limiter (`REDIS_URL`).
- If you deploy Envoy rate limiting, use a dedicated Redis/Valkey backend for Envoy instead of sharing the app cache.
- If application Redis is unavailable at runtime, app rate-limiter checks currently fail open (requests are allowed through without enforcement).
- Authentication failure audit logging uses a separate throttle (`shouldLogAuthFailure()`) and is intentionally **fail-closed**: when Redis is unavailable or errors occur, audit log entries are **skipped entirely** rather than written without throttle control. This prevents spam while preserving the hash-integrity chain required for compliance. In other words, if Redis is down, no authentication-failure audit logs will be recorded—requests themselves are still allowed (fail-open rate limiting above), but the audit trail for those failures will not be written.