mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 16:16:21 -06:00
chore: Replaces Unkey and Update rate limiting in the management API v2. (#6273)
This commit is contained in:
@@ -194,9 +194,6 @@ REDIS_URL=redis://localhost:6379
|
||||
# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this)
|
||||
# REDIS_HTTP_URL:
|
||||
|
||||
# The below is used for Rate Limiting for management API
|
||||
UNKEY_ROOT_KEY=
|
||||
|
||||
# INTERCOM_APP_ID=
|
||||
# INTERCOM_SECRET_KEY=
|
||||
|
||||
|
||||
@@ -175,7 +175,6 @@ export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY;
|
||||
export const REDIS_URL = env.REDIS_URL;
|
||||
export const REDIS_HTTP_URL = env.REDIS_HTTP_URL;
|
||||
export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1";
|
||||
export const UNKEY_ROOT_KEY = env.UNKEY_ROOT_KEY;
|
||||
|
||||
export const BREVO_API_KEY = env.BREVO_API_KEY;
|
||||
export const BREVO_LIST_ID = env.BREVO_LIST_ID;
|
||||
|
||||
@@ -116,7 +116,7 @@ export const env = createEnv({
|
||||
VERCEL_URL: z.string().optional(),
|
||||
WEBAPP_URL: z.string().url().optional(),
|
||||
UNSPLASH_ACCESS_KEY: z.string().optional(),
|
||||
UNKEY_ROOT_KEY: z.string().optional(),
|
||||
|
||||
NODE_ENV: z.enum(["development", "production", "test"]).optional(),
|
||||
PROMETHEUS_EXPORTER_PORT: z.string().optional(),
|
||||
PROMETHEUS_ENABLED: z.enum(["1", "0"]).optional(),
|
||||
@@ -218,7 +218,6 @@ export const env = createEnv({
|
||||
VERCEL_URL: process.env.VERCEL_URL,
|
||||
WEBAPP_URL: process.env.WEBAPP_URL,
|
||||
UNSPLASH_ACCESS_KEY: process.env.UNSPLASH_ACCESS_KEY,
|
||||
UNKEY_ROOT_KEY: process.env.UNKEY_ROOT_KEY,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
PROMETHEUS_ENABLED: process.env.PROMETHEUS_ENABLED,
|
||||
PROMETHEUS_EXPORTER_PORT: process.env.PROMETHEUS_EXPORTER_PORT,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiAuditLog } from "@/app/lib/api/with-api-logging";
|
||||
import { checkRateLimitAndThrowError } from "@/modules/api/v2/lib/rate-limit";
|
||||
import { formatZodError, handleApiError } from "@/modules/api/v2/lib/utils";
|
||||
import { applyRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { ZodRawShape, z } from "zod";
|
||||
import { TAuthenticationApiKey } from "@formbricks/types/auth";
|
||||
import { authenticateRequest } from "./authenticate-request";
|
||||
@@ -104,11 +105,10 @@ export const apiWrapper = async <S extends ExtendedSchemas>({
|
||||
}
|
||||
|
||||
if (rateLimit) {
|
||||
const rateLimitResponse = await checkRateLimitAndThrowError({
|
||||
identifier: authentication.data.hashedApiKey,
|
||||
});
|
||||
if (!rateLimitResponse.ok) {
|
||||
return handleApiError(request, rateLimitResponse.error);
|
||||
try {
|
||||
await applyRateLimit(rateLimitConfigs.api.v2, authentication.data.hashedApiKey);
|
||||
} catch (error) {
|
||||
return handleApiError(request, { type: "too_many_requests", details: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { apiWrapper } from "@/modules/api/v2/auth/api-wrapper";
|
||||
import { authenticateRequest } from "@/modules/api/v2/auth/authenticate-request";
|
||||
import { checkRateLimitAndThrowError } from "@/modules/api/v2/lib/rate-limit";
|
||||
import { handleApiError } from "@/modules/api/v2/lib/utils";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { checkRateLimit } from "@/modules/core/rate-limit/rate-limit";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { err, ok, okVoid } from "@formbricks/types/error-handlers";
|
||||
import { err, ok } from "@formbricks/types/error-handlers";
|
||||
|
||||
vi.mock("../authenticate-request", () => ({
|
||||
authenticateRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/api/v2/lib/rate-limit", () => ({
|
||||
checkRateLimitAndThrowError: vi.fn(),
|
||||
vi.mock("@/modules/core/rate-limit/rate-limit", () => ({
|
||||
checkRateLimit: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/api/v2/lib/utils", () => ({
|
||||
handleApiError: vi.fn(),
|
||||
vi.mock("@/modules/core/rate-limit/rate-limit-configs", () => ({
|
||||
rateLimitConfigs: {
|
||||
api: {
|
||||
v2: { interval: 60, allowedPerInterval: 100, namespace: "api:v2" },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/api/v2/lib/utils", () => ({
|
||||
@@ -24,20 +28,31 @@ vi.mock("@/modules/api/v2/lib/utils", () => ({
|
||||
handleApiError: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockAuthentication = {
|
||||
type: "apiKey" as const,
|
||||
environmentPermissions: [
|
||||
{
|
||||
environmentId: "env-id",
|
||||
environmentType: "development" as const,
|
||||
projectId: "project-id",
|
||||
projectName: "Project Name",
|
||||
permission: "manage" as const,
|
||||
},
|
||||
],
|
||||
hashedApiKey: "hashed-api-key",
|
||||
apiKeyId: "api-key-id",
|
||||
organizationId: "org-id",
|
||||
organizationAccess: {} as any,
|
||||
} as any;
|
||||
|
||||
describe("apiWrapper", () => {
|
||||
test("should handle request and return response", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "valid-api-key" },
|
||||
});
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
ok({
|
||||
type: "apiKey",
|
||||
environmentId: "env-id",
|
||||
hashedApiKey: "hashed-api-key",
|
||||
})
|
||||
);
|
||||
vi.mocked(checkRateLimitAndThrowError).mockResolvedValue(okVoid());
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
vi.mocked(checkRateLimit).mockResolvedValue(ok({ allowed: true }));
|
||||
|
||||
const handler = vi.fn().mockResolvedValue(new Response("ok", { status: 200 }));
|
||||
const response = await apiWrapper({
|
||||
@@ -74,13 +89,7 @@ describe("apiWrapper", () => {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
ok({
|
||||
type: "apiKey",
|
||||
environmentId: "env-id",
|
||||
hashedApiKey: "hashed-api-key",
|
||||
})
|
||||
);
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
|
||||
const bodySchema = z.object({ key: z.string() });
|
||||
const handler = vi.fn().mockResolvedValue(new Response("ok", { status: 200 }));
|
||||
@@ -107,14 +116,7 @@ describe("apiWrapper", () => {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
ok({
|
||||
type: "apiKey",
|
||||
environmentId: "env-id",
|
||||
hashedApiKey: "hashed-api-key",
|
||||
})
|
||||
);
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
vi.mocked(handleApiError).mockResolvedValue(new Response("error", { status: 400 }));
|
||||
|
||||
const bodySchema = z.object({ key: z.string() });
|
||||
@@ -134,13 +136,7 @@ describe("apiWrapper", () => {
|
||||
test("should parse query schema correctly", async () => {
|
||||
const request = new Request("http://localhost?key=value");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
ok({
|
||||
type: "apiKey",
|
||||
environmentId: "env-id",
|
||||
hashedApiKey: "hashed-api-key",
|
||||
})
|
||||
);
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
|
||||
const querySchema = z.object({ key: z.string() });
|
||||
const handler = vi.fn().mockResolvedValue(new Response("ok", { status: 200 }));
|
||||
@@ -163,14 +159,7 @@ describe("apiWrapper", () => {
|
||||
test("should handle query schema errors", async () => {
|
||||
const request = new Request("http://localhost?foo%ZZ=abc");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
ok({
|
||||
type: "apiKey",
|
||||
environmentId: "env-id",
|
||||
hashedApiKey: "hashed-api-key",
|
||||
})
|
||||
);
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
vi.mocked(handleApiError).mockResolvedValue(new Response("error", { status: 400 }));
|
||||
|
||||
const querySchema = z.object({ key: z.string() });
|
||||
@@ -190,13 +179,7 @@ describe("apiWrapper", () => {
|
||||
test("should parse params schema correctly", async () => {
|
||||
const request = new Request("http://localhost");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
ok({
|
||||
type: "apiKey",
|
||||
environmentId: "env-id",
|
||||
hashedApiKey: "hashed-api-key",
|
||||
})
|
||||
);
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
|
||||
const paramsSchema = z.object({ key: z.string() });
|
||||
const handler = vi.fn().mockResolvedValue(new Response("ok", { status: 200 }));
|
||||
@@ -220,14 +203,7 @@ describe("apiWrapper", () => {
|
||||
test("should handle no external params", async () => {
|
||||
const request = new Request("http://localhost");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
ok({
|
||||
type: "apiKey",
|
||||
environmentId: "env-id",
|
||||
hashedApiKey: "hashed-api-key",
|
||||
})
|
||||
);
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
vi.mocked(handleApiError).mockResolvedValue(new Response("error", { status: 400 }));
|
||||
|
||||
const paramsSchema = z.object({ key: z.string() });
|
||||
@@ -248,14 +224,7 @@ describe("apiWrapper", () => {
|
||||
test("should handle params schema errors", async () => {
|
||||
const request = new Request("http://localhost");
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
ok({
|
||||
type: "apiKey",
|
||||
environmentId: "env-id",
|
||||
hashedApiKey: "hashed-api-key",
|
||||
})
|
||||
);
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
vi.mocked(handleApiError).mockResolvedValue(new Response("error", { status: 400 }));
|
||||
|
||||
const paramsSchema = z.object({ key: z.string() });
|
||||
@@ -273,21 +242,13 @@ describe("apiWrapper", () => {
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle rate limit errors", async () => {
|
||||
test("should handle rate limit exceeded", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "valid-api-key" },
|
||||
});
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(
|
||||
ok({
|
||||
type: "apiKey",
|
||||
environmentId: "env-id",
|
||||
hashedApiKey: "hashed-api-key",
|
||||
})
|
||||
);
|
||||
vi.mocked(checkRateLimitAndThrowError).mockResolvedValue(
|
||||
err({ type: "rateLimitExceeded" } as unknown as ApiErrorResponseV2)
|
||||
);
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
vi.mocked(checkRateLimit).mockResolvedValue(ok({ allowed: false }));
|
||||
vi.mocked(handleApiError).mockImplementation(
|
||||
(_request: Request, _error: ApiErrorResponseV2): Response =>
|
||||
new Response("rate limit exceeded", { status: 429 })
|
||||
@@ -302,4 +263,24 @@ describe("apiWrapper", () => {
|
||||
expect(response.status).toBe(429);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle rate limit check failure gracefully", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
headers: { "x-api-key": "valid-api-key" },
|
||||
});
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
// When rate limiting fails (e.g., Redis connection issues), checkRateLimit fails open by returning allowed: true
|
||||
vi.mocked(checkRateLimit).mockResolvedValue(ok({ allowed: true }));
|
||||
|
||||
const handler = vi.fn().mockResolvedValue(new Response("ok", { status: 200 }));
|
||||
const response = await apiWrapper({
|
||||
request,
|
||||
handler,
|
||||
});
|
||||
|
||||
// Should fail open for availability
|
||||
expect(response.status).toBe(200);
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { MANAGEMENT_API_RATE_LIMIT, RATE_LIMITING_DISABLED, UNKEY_ROOT_KEY } from "@/lib/constants";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { type LimitOptions, Ratelimit, type RatelimitResponse } from "@unkey/ratelimit";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { Result, err, okVoid } from "@formbricks/types/error-handlers";
|
||||
|
||||
export type RateLimitHelper = {
|
||||
identifier: string;
|
||||
opts?: LimitOptions;
|
||||
/**
|
||||
* Using a callback instead of a regular return to provide headers even
|
||||
* when the rate limit is reached and an error is thrown.
|
||||
**/
|
||||
onRateLimiterResponse?: (response: RatelimitResponse) => void;
|
||||
};
|
||||
|
||||
let warningDisplayed = false;
|
||||
|
||||
/** Prevent flooding the logs while testing/building */
|
||||
function logOnce(message: string) {
|
||||
if (warningDisplayed) return;
|
||||
logger.warn(message);
|
||||
warningDisplayed = true;
|
||||
}
|
||||
|
||||
export function rateLimiter() {
|
||||
if (RATE_LIMITING_DISABLED) {
|
||||
logOnce("Rate limiting disabled");
|
||||
return () => ({ success: true, limit: 10, remaining: 999, reset: 0 }) as RatelimitResponse;
|
||||
}
|
||||
|
||||
if (!UNKEY_ROOT_KEY) {
|
||||
logOnce("Disabled due to not finding UNKEY_ROOT_KEY env variable");
|
||||
return () => ({ success: true, limit: 10, remaining: 999, reset: 0 }) as RatelimitResponse;
|
||||
}
|
||||
const timeout = {
|
||||
fallback: { success: true, limit: 10, remaining: 999, reset: 0 },
|
||||
ms: 5000,
|
||||
};
|
||||
|
||||
const limiter = {
|
||||
api: new Ratelimit({
|
||||
rootKey: UNKEY_ROOT_KEY,
|
||||
namespace: "api",
|
||||
limit: MANAGEMENT_API_RATE_LIMIT.allowedPerInterval,
|
||||
duration: MANAGEMENT_API_RATE_LIMIT.interval * 1000,
|
||||
timeout,
|
||||
}),
|
||||
};
|
||||
|
||||
async function rateLimit({ identifier, opts }: RateLimitHelper) {
|
||||
return await limiter.api.limit(identifier, opts);
|
||||
}
|
||||
|
||||
return rateLimit;
|
||||
}
|
||||
|
||||
export const checkRateLimitAndThrowError = async ({
|
||||
identifier,
|
||||
opts,
|
||||
}: RateLimitHelper): Promise<Result<void, ApiErrorResponseV2>> => {
|
||||
const response = await rateLimiter()({ identifier, opts });
|
||||
const { success } = response;
|
||||
|
||||
if (!success) {
|
||||
return err({
|
||||
type: "too_many_requests",
|
||||
});
|
||||
}
|
||||
return okVoid();
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@unkey/ratelimit", () => ({
|
||||
Ratelimit: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("when rate limiting is disabled", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
const constants = await vi.importActual("@/lib/constants");
|
||||
vi.doMock("@/lib/constants", () => ({
|
||||
...constants,
|
||||
MANAGEMENT_API_RATE_LIMIT: { allowedPerInterval: 5, interval: 60 },
|
||||
RATE_LIMITING_DISABLED: true,
|
||||
}));
|
||||
});
|
||||
|
||||
test("should log a warning once and return a stubbed response", async () => {
|
||||
const loggerSpy = vi.spyOn(logger, "warn");
|
||||
const { rateLimiter } = await import("@/modules/api/v2/lib/rate-limit");
|
||||
|
||||
const res1 = await rateLimiter()({ identifier: "test-id" });
|
||||
expect(res1).toEqual({ success: true, limit: 10, remaining: 999, reset: 0 });
|
||||
expect(loggerSpy).toHaveBeenCalled();
|
||||
|
||||
// Subsequent calls won't log again.
|
||||
await rateLimiter()({ identifier: "another-id" });
|
||||
|
||||
expect(loggerSpy).toHaveBeenCalledTimes(1);
|
||||
loggerSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when UNKEY_ROOT_KEY is missing", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
const constants = await vi.importActual("@/lib/constants");
|
||||
vi.doMock("@/lib/constants", () => ({
|
||||
...constants,
|
||||
MANAGEMENT_API_RATE_LIMIT: { allowedPerInterval: 5, interval: 60 },
|
||||
RATE_LIMITING_DISABLED: false,
|
||||
UNKEY_ROOT_KEY: "",
|
||||
}));
|
||||
});
|
||||
|
||||
test("should log a warning about missing UNKEY_ROOT_KEY and return stub response", async () => {
|
||||
const loggerSpy = vi.spyOn(logger, "warn");
|
||||
const { rateLimiter } = await import("@/modules/api/v2/lib/rate-limit");
|
||||
const limiterFunc = rateLimiter();
|
||||
|
||||
const res = await limiterFunc({ identifier: "test-id" });
|
||||
expect(res).toEqual({ success: true, limit: 10, remaining: 999, reset: 0 });
|
||||
expect(loggerSpy).toHaveBeenCalled();
|
||||
loggerSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rate limiting is active (enabled)", () => {
|
||||
const mockResponse = { success: true, limit: 5, remaining: 2, reset: 1000 };
|
||||
let limitMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
const constants = await vi.importActual("@/lib/constants");
|
||||
vi.doMock("@/lib/constants", () => ({
|
||||
...constants,
|
||||
MANAGEMENT_API_RATE_LIMIT: { allowedPerInterval: 5, interval: 60 },
|
||||
RATE_LIMITING_DISABLED: false,
|
||||
UNKEY_ROOT_KEY: "valid-key",
|
||||
}));
|
||||
|
||||
limitMock = vi.fn().mockResolvedValue(mockResponse);
|
||||
const RatelimitMock = vi.fn().mockImplementation(() => {
|
||||
return { limit: limitMock };
|
||||
});
|
||||
vi.doMock("@unkey/ratelimit", () => ({
|
||||
Ratelimit: RatelimitMock,
|
||||
}));
|
||||
});
|
||||
|
||||
test("should create a rate limiter that calls the limit method with the proper arguments", async () => {
|
||||
const { rateLimiter } = await import("../rate-limit");
|
||||
const limiterFunc = rateLimiter();
|
||||
const res = await limiterFunc({ identifier: "abc", opts: { cost: 1 } });
|
||||
expect(limitMock).toHaveBeenCalledWith("abc", { cost: 1 });
|
||||
expect(res).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test("checkRateLimitAndThrowError returns okVoid when rate limit is not exceeded", async () => {
|
||||
limitMock.mockResolvedValueOnce({ success: true, limit: 5, remaining: 3, reset: 1000 });
|
||||
|
||||
const { checkRateLimitAndThrowError } = await import("../rate-limit");
|
||||
const result = await checkRateLimitAndThrowError({ identifier: "abc" });
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
test("checkRateLimitAndThrowError returns an error when the rate limit is exceeded", async () => {
|
||||
limitMock.mockResolvedValueOnce({ success: false, limit: 5, remaining: 0, reset: 1000 });
|
||||
|
||||
const { checkRateLimitAndThrowError } = await import("../rate-limit");
|
||||
const result = await checkRateLimitAndThrowError({ identifier: "abc" });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toEqual({ type: "too_many_requests" });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -79,7 +79,6 @@
|
||||
"@tolgee/format-icu": "6.2.5",
|
||||
"@tolgee/react": "6.2.5",
|
||||
"@ungap/structured-clone": "1.3.0",
|
||||
"@unkey/ratelimit": "0.5.5",
|
||||
"@vercel/functions": "2.0.2",
|
||||
"@vercel/og": "0.6.8",
|
||||
"bcryptjs": "3.0.2",
|
||||
|
||||
@@ -54,7 +54,7 @@ These variables are present inside your machine's docker-compose file. Restart t
|
||||
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
|
||||
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
|
||||
| TELEMETRY_DISABLED | Disables telemetry if set to 1. | optional | |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
|
||||
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
|
||||
| DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | |
|
||||
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
|
||||
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
|
||||
@@ -62,7 +62,6 @@ These variables are present inside your machine's docker-compose file. Restart t
|
||||
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have .well-known configured at this) | optional (required if OIDC auth is enabled) | |
|
||||
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 |
|
||||
| OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | |
|
||||
| UNKEY_ROOT_KEY | Key for the [Unkey](https://www.unkey.com/) service. This is used for Rate Limiting for management API. | optional | |
|
||||
| PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | |
|
||||
| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 |
|
||||
| DOCKER_CRON_ENABLED | Controls whether cron jobs run in the Docker image. Set to 0 to disable (useful for cluster setups). | optional | 1 |
|
||||
|
||||
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@@ -277,9 +277,6 @@ importers:
|
||||
'@ungap/structured-clone':
|
||||
specifier: 1.3.0
|
||||
version: 1.3.0
|
||||
'@unkey/ratelimit':
|
||||
specifier: 0.5.5
|
||||
version: 0.5.5
|
||||
'@vercel/functions':
|
||||
specifier: 2.0.2
|
||||
version: 2.0.2(@aws-sdk/credential-provider-web-identity@3.804.0)
|
||||
@@ -4339,18 +4336,6 @@ packages:
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
'@unkey/api@0.33.1':
|
||||
resolution: {integrity: sha512-HXFQOGjO3S0+N6GMBOz9lkn2Fp41rdgNrafBkjRg8IjOsNv6SNdWrXZSELONinZYycr7Wxc/t7Vhx7xUdb0cdA==}
|
||||
|
||||
'@unkey/error@0.2.0':
|
||||
resolution: {integrity: sha512-DFGb4A7SrusZPP0FYuRIF0CO+Gi4etLUAEJ6EKc+TKYmscL0nEJ2Pr38FyX9MvjI4Wx5l35Wc9KsBjMm9Ybh7w==}
|
||||
|
||||
'@unkey/ratelimit@0.5.5':
|
||||
resolution: {integrity: sha512-79Xv7lZFHqScGzBQpO3hh2SZBhujXDDkyH/izWzeXcJ5sFYO+PsC+VaAFagEinbHbrx116RkIRIqbDrfZfRpxA==}
|
||||
|
||||
'@unkey/rbac@0.3.1':
|
||||
resolution: {integrity: sha512-Hj+52XRIlBBl3/qOUq9K71Fwy3PWExBQOpOClVYHdrcmbgqNL6L4EdW/BzliLhqPCdwZTPVSJTnZ3Hw4ZYixsQ==}
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
|
||||
cpu: [arm]
|
||||
@@ -14435,23 +14420,6 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@unkey/api@0.33.1':
|
||||
dependencies:
|
||||
'@unkey/rbac': 0.3.1
|
||||
|
||||
'@unkey/error@0.2.0':
|
||||
dependencies:
|
||||
zod: 3.24.4
|
||||
|
||||
'@unkey/ratelimit@0.5.5':
|
||||
dependencies:
|
||||
'@unkey/api': 0.33.1
|
||||
|
||||
'@unkey/rbac@0.3.1':
|
||||
dependencies:
|
||||
'@unkey/error': 0.2.0
|
||||
zod: 3.24.4
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
optional: true
|
||||
|
||||
|
||||
@@ -174,7 +174,6 @@
|
||||
"VERSION",
|
||||
"WEBAPP_URL",
|
||||
"UNSPLASH_ACCESS_KEY",
|
||||
"UNKEY_ROOT_KEY",
|
||||
"PROMETHEUS_ENABLED",
|
||||
"PROMETHEUS_EXPORTER_PORT",
|
||||
"USER_MANAGEMENT_MINIMUM_ROLE"
|
||||
|
||||
Reference in New Issue
Block a user