chore: clean-up new cache package (#6532)

This commit is contained in:
Victor Hugo dos Santos
2025-09-12 08:16:13 -03:00
committed by GitHub
parent 3879d86f63
commit 935e24bd43
26 changed files with 101 additions and 230 deletions

View File

@@ -16,9 +16,10 @@ Formbricks uses a **hybrid caching approach** optimized for enterprise scale:
## Key Files
### Core Cache Infrastructure
- [apps/web/modules/cache/lib/service.ts](mdc:apps/web/modules/cache/lib/service.ts) - Redis cache service
- [apps/web/modules/cache/lib/withCache.ts](mdc:apps/web/modules/cache/lib/withCache.ts) - Cache wrapper utilities
- [apps/web/modules/cache/lib/cacheKeys.ts](mdc:apps/web/modules/cache/lib/cacheKeys.ts) - Enterprise cache key patterns and utilities
- [packages/cache/src/service.ts](mdc:packages/cache/src/service.ts) - Redis cache service
- [packages/cache/src/client.ts](mdc:packages/cache/src/client.ts) - Cache client initialization and singleton management
- [apps/web/lib/cache/index.ts](mdc:apps/web/lib/cache/index.ts) - Cache service proxy for web app
- [packages/cache/src/index.ts](mdc:packages/cache/src/index.ts) - Cache package exports and utilities
### Environment State Caching (Critical Endpoint)
- [apps/web/app/api/v1/client/[environmentId]/environment/route.ts](mdc:apps/web/app/api/v1/client/[environmentId]/environment/route.ts) - Main endpoint serving hundreds of thousands of SDK clients
@@ -26,7 +27,7 @@ Formbricks uses a **hybrid caching approach** optimized for enterprise scale:
## Enterprise-Grade Cache Key Patterns
**Always use** the `createCacheKey` utilities from [cacheKeys.ts](mdc:apps/web/modules/cache/lib/cacheKeys.ts):
**Always use** the `createCacheKey` utilities from the cache package:
```typescript
// ✅ Correct patterns
@@ -50,14 +51,14 @@ export const getEnterpriseLicense = reactCache(async () => {
});
```
### Use `withCache()` for Simple Database Queries
### Use `cache.withCache()` for Simple Database Queries
```typescript
// ✅ Simple caching with automatic fallback (TTL in milliseconds)
export const getActionClasses = (environmentId: string) => {
return withCache(() => fetchActionClassesFromDB(environmentId), {
key: createCacheKey.environment.actionClasses(environmentId),
ttl: 60 * 30 * 1000, // 30 minutes in milliseconds
})();
return cache.withCache(() => fetchActionClassesFromDB(environmentId),
createCacheKey.environment.actionClasses(environmentId),
60 * 30 * 1000 // 30 minutes in milliseconds
);
};
```

View File

@@ -21,10 +21,10 @@ jobs:
name: Validate Docker Build
runs-on: ubuntu-latest
# Add PostgreSQL service container
# Add PostgreSQL and Redis service containers
services:
postgres:
image: pgvector/pgvector:pg17
image: pgvector/pgvector@sha256:9ae02a756ba16a2d69dd78058e25915e36e189bb36ddf01ceae86390d7ed786a
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
@@ -38,6 +38,11 @@ jobs:
--health-timeout 5s
--health-retries 5
redis:
image: valkey/valkey@sha256:12ba4f45a7c3e1d0f076acd616cb230834e75a77e8516dde382720af32832d6d
ports:
- 6379:6379
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
@@ -67,6 +72,7 @@ jobs:
secrets: |
database_url=${{ secrets.DUMMY_DATABASE_URL }}
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
redis_url=redis://localhost:6379
- name: Verify and Initialize PostgreSQL
run: |
@@ -96,6 +102,29 @@ jobs:
echo "Network configuration:"
netstat -tulpn | grep 5432 || echo "No process listening on port 5432"
- name: Verify Redis/Valkey Connection
run: |
echo "Verifying Redis/Valkey connection..."
# Install Redis client to test connection
sudo apt-get update && sudo apt-get install -y redis-tools
# Test connection using redis-cli with timeout and proper error handling
echo "Testing Redis connection with 30 second timeout..."
if timeout 30 bash -c 'until redis-cli -h localhost -p 6379 ping >/dev/null 2>&1; do
echo "Waiting for Redis to be ready..."
sleep 2
done'; then
echo "✅ Redis connection successful"
redis-cli -h localhost -p 6379 info server | head -5
else
echo "❌ Redis connection failed after 30 seconds"
exit 1
fi
# Show network configuration for Redis
echo "Redis network configuration:"
netstat -tulpn | grep 6379 || echo "No process listening on port 6379"
- name: Test Docker Image with Health Check
shell: bash
env:
@@ -113,6 +142,7 @@ jobs:
-p 3000:3000 \
-e DATABASE_URL="postgresql://test:test@host.docker.internal:5432/formbricks" \
-e ENCRYPTION_KEY="$DUMMY_ENCRYPTION_KEY" \
-e REDIS_URL="redis://host.docker.internal:6379" \
-d "formbricks-test:$GITHUB_SHA"
# Start health check polling immediately (every 5 seconds for up to 5 minutes)

View File

@@ -46,15 +46,9 @@ jobs:
--health-timeout=5s
--health-retries=5
valkey:
image: valkey/valkey:8.1.1
image: valkey/valkey@sha256:12ba4f45a7c3e1d0f076acd616cb230834e75a77e8516dde382720af32832d6d
ports:
- 6379:6379
options: >-
--entrypoint "valkey-server"
--health-cmd="valkey-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
minio:
image: bitnami/minio:2025.7.23-debian-12-r5
env:

View File

@@ -61,6 +61,7 @@ RUN pnpm build --filter=@formbricks/database
# This mounts the secrets only during this build step without storing them in layers
RUN --mount=type=secret,id=database_url \
--mount=type=secret,id=encryption_key \
--mount=type=secret,id=redis_url \
--mount=type=secret,id=sentry_auth_token \
/tmp/read-secrets.sh pnpm build --filter=@formbricks/web...

View File

@@ -151,7 +151,6 @@ export const DEBUG = env.DEBUG === "1";
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 BREVO_API_KEY = env.BREVO_API_KEY;

View File

@@ -54,8 +54,10 @@ export const env = createEnv({
OIDC_ISSUER: z.string().optional(),
OIDC_SIGNING_ALGORITHM: z.string().optional(),
OPENTELEMETRY_LISTENER_URL: z.string().optional(),
REDIS_URL: z.string().optional(),
REDIS_HTTP_URL: z.string().optional(),
REDIS_URL:
process.env.NODE_ENV === "test"
? z.string().optional()
: z.string().url("REDIS_URL is required for caching, rate limiting, and audit logging"),
PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
POSTHOG_API_HOST: z.string().optional(),
POSTHOG_API_KEY: z.string().optional(),
@@ -182,7 +184,6 @@ export const env = createEnv({
OIDC_ISSUER: process.env.OIDC_ISSUER,
OIDC_SIGNING_ALGORITHM: process.env.OIDC_SIGNING_ALGORITHM,
REDIS_URL: process.env.REDIS_URL,
REDIS_HTTP_URL: process.env.REDIS_HTTP_URL,
PASSWORD_RESET_DISABLED: process.env.PASSWORD_RESET_DISABLED,
PRIVACY_URL: process.env.PRIVACY_URL,
RATE_LIMITING_DISABLED: process.env.RATE_LIMITING_DISABLED,

View File

@@ -1,15 +1,21 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { deepDiff, redactPII, sanitizeUrlForLogging } from "./logger-helpers";
// Patch redis multi before any imports
// Patch cache before any imports
beforeEach(async () => {
const redis = (await import("@/modules/cache/redis")).default;
if ((redis?.multi as any)?.mockReturnValue) {
(redis?.multi as any).mockReturnValue({
set: vi.fn(),
exec: vi.fn().mockResolvedValue([["OK"]]),
});
}
// Mock the cache service for tests
vi.doMock("@/lib/cache", () => ({
cache: {
getRedisClient: vi.fn().mockResolvedValue({
multi: vi.fn().mockReturnValue({
set: vi.fn(),
exec: vi.fn().mockResolvedValue([["OK"]]),
}),
watch: vi.fn().mockResolvedValue("OK"),
get: vi.fn().mockResolvedValue(null),
}),
},
}));
});
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
@@ -37,16 +43,7 @@ vi.mock("@/modules/ee/audit-logs/lib/service", () => ({
logAuditEvent: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/modules/cache/redis", () => ({
default: {
watch: vi.fn().mockResolvedValue("OK"),
multi: vi.fn().mockReturnValue({
set: vi.fn(),
exec: vi.fn().mockResolvedValue([["OK"]]),
}),
get: vi.fn().mockResolvedValue(null),
},
}));
// Cache mock is handled in beforeEach above
// Set ENCRYPTION_KEY for all tests unless explicitly testing its absence
process.env.ENCRYPTION_KEY = "testsecret";

View File

@@ -1,58 +0,0 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
describe("in-memory rate limiter", () => {
test("allows requests within limit and throws after limit", async () => {
const { rateLimit } = await import("./rate-limit");
const limiterFn = rateLimit({ interval: 1, allowedPerInterval: 2 });
await expect(limiterFn("a")).resolves.toBeUndefined();
await expect(limiterFn("a")).resolves.toBeUndefined();
await expect(limiterFn("a")).rejects.toThrow("Rate limit exceeded");
});
test("separate tokens have separate counts", async () => {
const { rateLimit } = await import("./rate-limit");
const limiterFn = rateLimit({ interval: 1, allowedPerInterval: 2 });
await expect(limiterFn("x")).resolves.toBeUndefined();
await expect(limiterFn("y")).resolves.toBeUndefined();
await expect(limiterFn("x")).resolves.toBeUndefined();
await expect(limiterFn("y")).resolves.toBeUndefined();
});
});
describe("redis rate limiter", () => {
beforeEach(async () => {
vi.resetModules();
const constants = await vi.importActual("@/lib/constants");
vi.doMock("@/lib/constants", () => ({
...constants,
REDIS_HTTP_URL: "http://redis",
}));
});
test("sets expire on first use and does not throw", async () => {
global.fetch = vi
.fn()
.mockResolvedValueOnce({ ok: true, json: async () => ({ INCR: 1 }) })
.mockResolvedValueOnce({ ok: true });
const { rateLimit } = await import("./rate-limit");
const limiter = rateLimit({ interval: 10, allowedPerInterval: 2 });
await expect(limiter("t")).resolves.toBeUndefined();
expect(fetch).toHaveBeenCalledTimes(2);
expect(fetch).toHaveBeenCalledWith("http://redis/INCR/t");
expect(fetch).toHaveBeenCalledWith("http://redis/EXPIRE/t/10");
});
test("does not throw when redis INCR response not ok", async () => {
global.fetch = vi.fn().mockResolvedValueOnce({ ok: false });
const { rateLimit } = await import("./rate-limit");
const limiter = rateLimit({ interval: 10, allowedPerInterval: 2 });
await expect(limiter("t")).resolves.toBeUndefined();
});
test("throws when INCR exceeds limit", async () => {
global.fetch = vi.fn().mockResolvedValueOnce({ ok: true, json: async () => ({ INCR: 3 }) });
const { rateLimit } = await import("./rate-limit");
const limiter = rateLimit({ interval: 10, allowedPerInterval: 2 });
await expect(limiter("t")).rejects.toThrow("Rate limit exceeded for IP: t");
});
});

View File

@@ -1,54 +0,0 @@
import { REDIS_HTTP_URL } from "@/lib/constants";
import { LRUCache } from "lru-cache";
import { logger } from "@formbricks/logger";
interface Options {
interval: number;
allowedPerInterval: number;
}
const inMemoryRateLimiter = (options: Options) => {
const tokenCache = new LRUCache<string, number>({
max: 1000,
ttl: options.interval * 1000, // converts to expected input of milliseconds
});
return async (token: string) => {
const currentUsage = tokenCache.get(token) ?? 0;
if (currentUsage >= options.allowedPerInterval) {
throw new Error("Rate limit exceeded");
}
tokenCache.set(token, currentUsage + 1);
};
};
const redisRateLimiter = (options: Options) => async (token: string) => {
try {
if (!REDIS_HTTP_URL) {
throw new Error("Redis HTTP URL is not set");
}
const tokenCountResponse = await fetch(`${REDIS_HTTP_URL}/INCR/${token}`);
if (!tokenCountResponse.ok) {
logger.error({ tokenCountResponse }, "Failed to increment token count in Redis");
return;
}
const { INCR } = await tokenCountResponse.json();
if (INCR === 1) {
await fetch(`${REDIS_HTTP_URL}/EXPIRE/${token}/${options.interval.toString()}`);
} else if (INCR > options.allowedPerInterval) {
throw new Error();
}
} catch (e) {
logger.error({ error: e }, "Rate limit exceeded");
throw new Error("Rate limit exceeded for IP: " + token);
}
};
export const rateLimit = (options: Options) => {
if (REDIS_HTTP_URL) {
return redisRateLimiter(options);
} else {
return inMemoryRateLimiter(options);
}
};

View File

@@ -748,7 +748,6 @@
"api_key_label": "API-Schlüssel Label",
"api_key_security_warning": "Aus Sicherheitsgründen wird der API-Schlüssel nur einmal nach der Erstellung angezeigt. Bitte kopiere ihn sofort an einen sicheren Ort.",
"api_key_updated": "API-Schlüssel aktualisiert",
"delete_permission": "Berechtigung löschen",
"duplicate_access": "Doppelter Projektzugriff nicht erlaubt",
"no_api_keys_yet": "Du hast noch keine API-Schlüssel",
"no_env_permissions_found": "Keine Umgebungsberechtigungen gefunden",

View File

@@ -748,7 +748,6 @@
"api_key_label": "API Key Label",
"api_key_security_warning": "For security reasons, the API key will only be shown once after creation. Please copy it to your destination right away.",
"api_key_updated": "API Key updated",
"delete_permission": "Delete permission",
"duplicate_access": "Duplicate project access not allowed",
"no_api_keys_yet": "You don't have any API keys yet",
"no_env_permissions_found": "No environment permissions found",

View File

@@ -748,7 +748,6 @@
"api_key_label": "Étiquette de clé API",
"api_key_security_warning": "Pour des raisons de sécurité, la clé API ne sera affichée qu'une seule fois après sa création. Veuillez la copier immédiatement à votre destination.",
"api_key_updated": "Clé API mise à jour",
"delete_permission": "Supprimer une permission",
"duplicate_access": "L'accès en double au projet n'est pas autorisé",
"no_api_keys_yet": "Vous n'avez pas encore de clés API.",
"no_env_permissions_found": "Aucune autorisation d'environnement trouvée",

View File

@@ -748,7 +748,6 @@
"api_key_label": "APIキーのラベル",
"api_key_security_warning": "セキュリティ上の理由から、APIキーは作成時に一度だけ表示されます。すぐにコピーしてください。",
"api_key_updated": "APIキーを更新しました",
"delete_permission": "削除権限",
"duplicate_access": "重複したプロジェクトアクセスは許可されません",
"no_api_keys_yet": "まだAPIキーがありません",
"no_env_permissions_found": "環境権限が見つかりません",

View File

@@ -748,7 +748,6 @@
"api_key_label": "Rótulo da Chave API",
"api_key_security_warning": "Por motivos de segurança, a chave da API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
"api_key_updated": "Chave de API atualizada",
"delete_permission": "Remover permissão",
"duplicate_access": "Acesso duplicado ao projeto não permitido",
"no_api_keys_yet": "Você ainda não tem nenhuma chave de API",
"no_env_permissions_found": "Nenhuma permissão de ambiente encontrada",

View File

@@ -748,7 +748,6 @@
"api_key_label": "Etiqueta da Chave API",
"api_key_security_warning": "Por razões de segurança, a chave API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
"api_key_updated": "Chave API atualizada",
"delete_permission": "Eliminar permissão",
"duplicate_access": "Acesso duplicado ao projeto não permitido",
"no_api_keys_yet": "Ainda não tem nenhuma chave API",
"no_env_permissions_found": "Nenhuma permissão de ambiente encontrada",

View File

@@ -748,7 +748,6 @@
"api_key_label": "Etichetă Cheie API",
"api_key_security_warning": "Din motive de securitate, cheia API va fi afișată o singură dată după creare. Vă rugăm să o copiați imediat la destinație.",
"api_key_updated": "Cheie API actualizată",
"delete_permission": "Șterge permisiunea",
"duplicate_access": "Accesul dublu la proiect nu este permis",
"no_api_keys_yet": "Nu aveți încă chei API",
"no_env_permissions_found": "Nu s-au găsit permisiuni pentru mediu",

View File

@@ -748,7 +748,6 @@
"api_key_label": "API Key Label",
"api_key_security_warning": "出于安全原因API 密钥在创建后只会显示一次。 请立即将其复制到您的目标位置。",
"api_key_updated": "API Key 已更新",
"delete_permission": "删除 权限",
"duplicate_access": "不允许 复制 项目 访问",
"no_api_keys_yet": "您 还 没有 任 何 API key",
"no_env_permissions_found": "未找到环境权限",

View File

@@ -748,7 +748,6 @@
"api_key_label": "API 金鑰標籤",
"api_key_security_warning": "為安全起見API 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。",
"api_key_updated": "API 金鑰已更新",
"delete_permission": "刪除 權限",
"duplicate_access": "不允許重複的 project 存取",
"no_api_keys_yet": "您還沒有任何 API 金鑰",
"no_env_permissions_found": "找不到環境權限",

View File

@@ -38,7 +38,6 @@
"@hookform/resolvers": "5.0.1",
"@intercom/messenger-js-sdk": "0.0.14",
"@json2csv/node": "7.0.6",
"@keyv/redis": "4.4.0",
"@lexical/code": "0.31.0",
"@lexical/link": "0.31.0",
"@lexical/list": "0.31.0",
@@ -85,7 +84,6 @@
"@vercel/og": "0.8.5",
"bcryptjs": "3.0.2",
"boring-avatars": "2.0.1",
"cache-manager": "6.4.3",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
@@ -98,7 +96,6 @@
"https-proxy-agent": "7.0.6",
"jiti": "2.4.2",
"jsonwebtoken": "9.0.2",
"keyv": "5.3.3",
"lexical": "0.31.0",
"lodash": "4.17.21",
"lru-cache": "11.1.0",

View File

@@ -6,7 +6,7 @@ if [ -f "/run/secrets/database_url" ]; then
DATABASE_URL=${DATABASE_URL%$'\n'}
export DATABASE_URL
else
echo "DATABASE_URL secret not found. Build may fail if this is required."
echo "DATABASE_URL secret not found. Build will fail because it is required by the application."
fi
if [ -f "/run/secrets/encryption_key" ]; then
@@ -14,7 +14,15 @@ if [ -f "/run/secrets/encryption_key" ]; then
ENCRYPTION_KEY=${ENCRYPTION_KEY%$'\n'}
export ENCRYPTION_KEY
else
echo "ENCRYPTION_KEY secret not found. Build may fail if this is required."
echo "ENCRYPTION_KEY secret not found. Build will fail because it is required by the application."
fi
if [ -f "/run/secrets/redis_url" ]; then
IFS= read -r REDIS_URL < /run/secrets/redis_url || true
REDIS_URL=${REDIS_URL%$'\n'}
export REDIS_URL
else
echo "REDIS_URL secret not found. Build will fail because it is required by the application."
fi
if [ -f "/run/secrets/sentry_auth_token" ]; then
@@ -39,6 +47,7 @@ fi
# Verify environment variables are set before starting build
echo " DATABASE_URL: $([ -n "${DATABASE_URL:-}" ] && printf '[SET]' || printf '[NOT SET]')"
echo " ENCRYPTION_KEY: $([ -n "${ENCRYPTION_KEY:-}" ] && printf '[SET]' || printf '[NOT SET]')"
echo " REDIS_URL: $([ -n "${REDIS_URL:-}" ] && printf '[SET]' || printf '[NOT SET]')"
echo " SENTRY_AUTH_TOKEN: $([ -n "${SENTRY_AUTH_TOKEN:-}" ] && printf '[SET]' || printf '[NOT SET]')"
echo " TARGETARCH: $([ -n "${TARGETARCH:-}" ] && printf '%s' "${TARGETARCH}" || printf '[NOT SET]')"

View File

@@ -17,7 +17,7 @@ services:
- 1025:1025
valkey:
image: valkey/valkey:8.1.1
image: valkey/valkey@sha256:12ba4f45a7c3e1d0f076acd616cb230834e75a77e8516dde382720af32832d6d
command: "valkey-server"
ports:
- 6379:6379

View File

@@ -26,6 +26,10 @@ x-environment: &environment
# You can use: $(openssl rand -hex 32) to generate a secure one
CRON_SECRET:
# Redis URL for caching, rate limiting, and audit logging
# To use external Redis/Valkey: remove the redis service below and update this URL
REDIS_URL: redis://redis:6379
# Set the minimum log level(debug, info, warn, error, fatal)
# LOG_LEVEL: info
@@ -169,12 +173,6 @@ x-environment: &environment
# Set the below to send OpenTelemetry data for tracing
# OPENTELEMETRY_LISTENER_URL: http://localhost:4318/v1/traces
# Set the below to use Redis for Next Caching (default is In-Memory from Next Cache)
# REDIS_URL:
# Set the below to use for Rate Limiting (default us In-Memory LRU Cache)
# REDIS_HTTP_URL:
########################################## OPTIONAL (AUDIT LOGGING) ###########################################
# Set the below to 1 to enable audit logging.
@@ -208,17 +206,35 @@ services:
# Replace the below with your own secure password & Make sure the password matches the password field in DATABASE_URL above
- POSTGRES_PASSWORD=postgres
# Redis/Valkey service for caching, rate limiting, and audit logging
# Remove this service if you want to use an external Redis/Valkey instance
redis:
restart: always
image: valkey/valkey@sha256:12ba4f45a7c3e1d0f076acd616cb230834e75a77e8516dde382720af32832d6d
command: valkey-server --appendonly yes
volumes:
- redis:/data
ports:
- "6379:6379"
formbricks:
restart: always
image: ghcr.io/formbricks/formbricks:latest
depends_on:
- postgres
- redis
ports:
- 3000:3000
volumes:
- ./saml-connection:/home/nextjs/apps/web/saml-connection
<<: *environment
volumes:
postgres:
driver: local
redis:
driver: local
uploads:
driver: local

View File

@@ -53,7 +53,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) | |
@@ -69,8 +69,7 @@ These variables are present inside your machine's docker-compose file. Restart t
| SENTRY_AUTH_TOKEN | Set this if you want to make errors more readable in Sentry. | optional | |
| SESSION_MAX_AGE | Configure the maximum age for the session in seconds. | optional | 86400 (24 hours) |
| USER_MANAGEMENT_MINIMUM_ROLE | Set this to control which roles can access user management features. Accepted values: "owner", "manager", "disabled" | optional | manager |
| REDIS_URL | Redis URL for caching and audit logging. Required for audit logging and optional for Next.js caching. | optional (required if audit logging is enabled) | |
| REDIS_HTTP_URL | Redis URL for rate limiting. If not set, rate limiting uses in-memory LRU cache. | optional | |
| REDIS_URL | Redis URL for caching, rate limiting, and audit logging. Application will not start without this. | required | redis://localhost:6379 |
| AUDIT_LOG_ENABLED | Set this to 1 to enable audit logging. Requires Redis to be configured with the REDIS_URL env variable. | optional | 0 |
| AUDIT_LOG_GET_USER_IP | Set to 1 to include user IP addresses in audit logs from request headers | optional | 0 |

View File

@@ -120,13 +120,12 @@ graph TD
## Redis Configuration
<Note>Redis integration is an Enterprise Edition feature and requires an enterprise license key.</Note>
<Note>Redis is required for Formbricks to function. The application will not start without a Redis URL configured.</Note>
Configure Redis by adding the following environment variables to your instances:
Configure Redis by adding the following **required** environment variable to your instances:
```sh env
REDIS_URL=redis://your-redis-host:6379
REDIS_HTTP_URL=http://your-redis-host:8000
```
## S3 Configuration

50
pnpm-lock.yaml generated
View File

@@ -159,9 +159,6 @@ importers:
'@json2csv/node':
specifier: 7.0.6
version: 7.0.6
'@keyv/redis':
specifier: 4.4.0
version: 4.4.0(keyv@5.3.3)
'@lexical/code':
specifier: 0.31.0
version: 0.31.0
@@ -300,9 +297,6 @@ importers:
boring-avatars:
specifier: 2.0.1
version: 2.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
cache-manager:
specifier: 6.4.3
version: 6.4.3
class-variance-authority:
specifier: 0.7.1
version: 0.7.1
@@ -339,9 +333,6 @@ importers:
jsonwebtoken:
specifier: 9.0.2
version: 9.0.2
keyv:
specifier: 5.3.3
version: 5.3.3
lexical:
specifier: 0.31.0
version: 0.31.0
@@ -2046,15 +2037,6 @@ packages:
'@json2csv/plainjs@7.0.6':
resolution: {integrity: sha512-4Md7RPDCSYpmW1HWIpWBOqCd4vWfIqm53S3e/uzQ62iGi7L3r34fK/8nhOMEe+/eVfCx8+gdSCt1d74SlacQHw==}
'@keyv/redis@4.4.0':
resolution: {integrity: sha512-n/KEj3S7crVkoykggqsMUtcjNGvjagGPlJYgO/r6m9hhGZfhp1txJElHxcdJ1ANi/LJoBuOSILj15g6HD2ucqQ==}
engines: {node: '>= 18'}
peerDependencies:
keyv: ^5.3.3
'@keyv/serialize@1.1.0':
resolution: {integrity: sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g==}
'@lexical/clipboard@0.31.0':
resolution: {integrity: sha512-oOZgDnzD68uZMy99H1dx6Le/uJNxmvXgbHb2Qp6Zrej7OUnCV/X9kzVSgLqWKytgrjsyUjTLjRhkQgwLhu61sA==}
@@ -3356,10 +3338,6 @@ packages:
resolution: {integrity: sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==}
engines: {node: '>=14'}
'@redis/client@1.6.1':
resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==}
engines: {node: '>=14'}
'@redis/client@5.8.1':
resolution: {integrity: sha512-hD5Tvv7G0t8b3w8ao3kQ4jEPUmUUC6pqA18c8ciYF5xZGfUGBg0olQHW46v6qSt4O5bxOuB3uV7pM6H5wEjBwA==}
engines: {node: '>= 18'}
@@ -5429,9 +5407,6 @@ packages:
resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==}
engines: {node: '>= 10'}
cache-manager@6.4.3:
resolution: {integrity: sha512-VV5eq/QQ5rIVix7/aICO4JyvSeEv9eIQuKL5iFwgM2BrcYoE0A/D1mNsAHJAsB0WEbNdBlKkn6Tjz6fKzh/cKQ==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -7270,9 +7245,6 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
keyv@5.3.3:
resolution: {integrity: sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==}
kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
@@ -11980,14 +11952,6 @@ snapshots:
'@json2csv/formatters': 7.0.6
'@streamparser/json': 0.0.20
'@keyv/redis@4.4.0(keyv@5.3.3)':
dependencies:
'@redis/client': 1.6.1
cluster-key-slot: 1.1.2
keyv: 5.3.3
'@keyv/serialize@1.1.0': {}
'@lexical/clipboard@0.31.0':
dependencies:
'@lexical/html': 0.31.0
@@ -13487,12 +13451,6 @@ snapshots:
generic-pool: 3.9.0
yallist: 4.0.0
'@redis/client@1.6.1':
dependencies:
cluster-key-slot: 1.1.2
generic-pool: 3.9.0
yallist: 4.0.0
'@redis/client@5.8.1':
dependencies:
cluster-key-slot: 1.1.2
@@ -16011,10 +15969,6 @@ snapshots:
- bluebird
optional: true
cache-manager@6.4.3:
dependencies:
keyv: 5.3.3
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -18097,10 +18051,6 @@ snapshots:
dependencies:
json-buffer: 3.0.1
keyv@5.3.3:
dependencies:
'@keyv/serialize': 1.1.0
kolorist@1.8.0: {}
language-subtag-registry@0.3.23: {}

View File

@@ -176,7 +176,6 @@
"POSTHOG_API_KEY",
"PRIVACY_URL",
"RATE_LIMITING_DISABLED",
"REDIS_HTTP_URL",
"REDIS_URL",
"S3_ACCESS_KEY",
"S3_BUCKET_NAME",