Repository: formbricks/formbricks
Pull request #5717: fix: nextjs cache handler for next 15
Author: login: mattinannt
Created: 2025-05-08T13:34:26Z | Merged: 2025-05-09T15:51:57Z

This PR aims to (temporarily) fix the Redis cache in Formbricks for the use of Nextjs 15.
To simplify the setup, we also remove the `CUSTOM_CACHE_DISABLED` environment variable.

We also removed the `REDIS_DEFAULT_TTL` environment variable to simplify the setup as this was a temporary fix for the previous caching issue.

This pull request introduces a significant overhaul to the caching system in the project by integrating the `@fortedigital/nextjs-cache-handler` library and removing the previous custom cache handler. It also includes related updates to configuration files, dependencies, and documentation. Below is a summary of the most important changes:

### Caching System Overhaul:
* Replaced the custom cache handler implementation in `apps/web/cache-handler.mjs` with the `@fortedigital/nextjs-cache-handler` library, introducing a composite handler that combines Redis and LRU caching strategies. This ensures better scalability and flexibility.
* Updated `apps/web/next.config.mjs` to use the new cache handler by default and removed the conditional logic for disabling the custom cache handler. [[1]](diffhunk://#diff-197cd8ca285a4abd2f21479e0bf6e36e90b08528fcd7f3bdbe8d1221897e377dR19-R20) [[2]](diffhunk://#diff-197cd8ca285a4abd2f21479e0bf6e36e90b08528fcd7f3bdbe8d1221897e377dL285-L289)

### Dependency Updates:
* Added `@fortedigital/nextjs-cache-handler` version 1.2.0 to `apps/web/package.json` and updated the `pnpm-lock.yaml` file to reflect this new dependency. [[1]](diffhunk://#diff-14b60f636e1a2b0061da57aaf231cb1ed15a5dc0c592425ed82e58fec95d42d8R35) [[2]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR154-R156) [[3]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR1685-R1690) [[4]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR11816-R11825)

### Configuration and Documentation Updates:
* Removed the `CUSTOM_CACHE_DISABLED` environment variable from `.env.example`, `docs/self-hosting/configuration/environment-variables.mdx`, and `turbo.json`, as it is no longer relevant with the new caching system. [[1]](diffhunk://#diff-a3046da0d15a27e89f2afe639b25748a7ad4d9290af3e7b1b6c1a5533c8f0a8cL203-L205) [[2]](diffhunk://#diff-94e29af518799a08a09c9c46017714105df45861563368ad4a1023683013536cL66) [[3]](diffhunk://#diff-f8de965273949793edc0fbfe249bb458c0becde39b2e141db087bcbf5d4ad5e3L80)

### Code Formatting:
* Applied consistent formatting in `apps/web/next.config.mjs` for better readability.

These changes collectively enhance the caching mechanism, improve maintainability, and align the project with modern caching practices.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
  - Enhanced caching with a composite strategy that intelligently uses both Redis and in-memory caching for improved reliability and performance.

- **Refactor**
  - Improved cache handler management for better Redis lifecycle handling and alignment with Next.js 15 caching best practices.
  - Updated Docker development environment by replacing Redis with Valkey service.
  - Made cache handler always enabled in configuration for consistent caching behavior.

- **Chores**
  - Updated dependencies to include the new cache handler package.
  - Removed unused environment variables related to cache TTL and disabling the custom cache handler from configuration, environment examples, and documentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.env.example b/.env.example
index 11b4efd35a0f..1e85f10ea2f6 100644
--- a/.env.example
+++ b/.env.example
@@ -191,8 +191,7 @@ UNSPLASH_ACCESS_KEY=
 
 # The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
 # You can also add more configuration to Redis using the redis.conf file in the root directory
-REDIS_URL=redis://localhost:6379
-REDIS_DEFAULT_TTL=86400 # 1 day 
+# 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:
@@ -200,9 +199,6 @@ REDIS_DEFAULT_TTL=86400 # 1 day
 # The below is used for Rate Limiting for management API
 UNKEY_ROOT_KEY=
 
-# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
-# CUSTOM_CACHE_DISABLED=1
-
 # INTERCOM_APP_ID=
 # INTERCOM_SECRET_KEY=
 
diff --git a/apps/web/cache-handler.js b/apps/web/cache-handler.js
new file mode 100644
index 000000000000..9a8ecdd59c06
--- /dev/null
+++ b/apps/web/cache-handler.js
@@ -0,0 +1,97 @@
+// This cache handler follows the @fortedigital/nextjs-cache-handler example
+// Read more at: https://github.com/fortedigital/nextjs-cache-handler
+
+// @neshca/cache-handler dependencies
+const { CacheHandler } = require("@neshca/cache-handler");
+const createLruHandler = require("@neshca/cache-handler/local-lru").default;
+
+// Next/Redis dependencies
+const { createClient } = require("redis");
+const { PHASE_PRODUCTION_BUILD } = require("next/constants");
+
+// @fortedigital/nextjs-cache-handler dependencies
+const createRedisHandler = require("@fortedigital/nextjs-cache-handler/redis-strings").default;
+const { Next15CacheHandler } = require("@fortedigital/nextjs-cache-handler/next-15-cache-handler");
+
+// Usual onCreation from @neshca/cache-handler
+CacheHandler.onCreation(() => {
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures only one instance get created
+  if (global.cacheHandlerConfig) {
+    return global.cacheHandlerConfig;
+  }
+
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures new instances are not created in a race condition
+  if (global.cacheHandlerConfigPromise) {
+    return global.cacheHandlerConfigPromise;
+  }
+
+  // If REDIS_URL is not set, we will use LRU cache only
+  if (!process.env.REDIS_URL) {
+    const lruCache = createLruHandler();
+    return { handlers: [lruCache] };
+  }
+
+  // Main promise initializing the handler
+  global.cacheHandlerConfigPromise = (async () => {
+    /** @type {import("redis").RedisClientType | null} */
+    let redisClient = null;
+    // eslint-disable-next-line turbo/no-undeclared-env-vars -- Next.js will inject this variable
+    if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
+      const settings = {
+        url: process.env.REDIS_URL, // Make sure you configure this variable
+        pingInterval: 10000,
+      };
+
+      try {
+        redisClient = createClient(settings);
+        redisClient.on("error", (e) => {
+          console.error("Redis error", e);
+          global.cacheHandlerConfig = null;
+          global.cacheHandlerConfigPromise = null;
+        });
+      } catch (error) {
+        console.error("Failed to create Redis client:", error);
+      }
+    }
+
+    if (redisClient) {
+      try {
+        console.info("Connecting Redis client...");
+        await redisClient.connect();
+        console.info("Redis client connected.");
+      } catch (error) {
+        console.error("Failed to connect Redis client:", error);
+        await redisClient
+          .disconnect()
+          .catch(() => console.error("Failed to quit the Redis client after failing to connect."));
+      }
+    }
+    const lruCache = createLruHandler();
+
+    if (!redisClient?.isReady) {
+      console.error("Failed to initialize caching layer.");
+      global.cacheHandlerConfigPromise = null;
+      global.cacheHandlerConfig = { handlers: [lruCache] };
+      return global.cacheHandlerConfig;
+    }
+
+    const redisCacheHandler = createRedisHandler({
+      client: redisClient,
+      keyPrefix: "nextjs:",
+    });
+
+    global.cacheHandlerConfigPromise = null;
+
+    global.cacheHandlerConfig = {
+      handlers: [redisCacheHandler],
+    };
+
+    return global.cacheHandlerConfig;
+  })();
+
+  return global.cacheHandlerConfigPromise;
+});
+
+module.exports = new Next15CacheHandler();
diff --git a/apps/web/cache-handler.mjs b/apps/web/cache-handler.mjs
deleted file mode 100644
index 1065fa3b832b..000000000000
--- a/apps/web/cache-handler.mjs
+++ /dev/null
@@ -1,79 +0,0 @@
-import { CacheHandler } from "@neshca/cache-handler";
-import createLruHandler from "@neshca/cache-handler/local-lru";
-import createRedisHandler from "@neshca/cache-handler/redis-strings";
-import { createClient } from "redis";
-
-// Function to create a timeout promise
-const createTimeoutPromise = (ms, rejectReason) => {
-  return new Promise((_, reject) => setTimeout(() => reject(new Error(rejectReason)), ms));
-};
-
-CacheHandler.onCreation(async () => {
-  let client;
-
-  if (process.env.REDIS_URL) {
-    try {
-      // Create a Redis client.
-      client = createClient({
-        url: process.env.REDIS_URL,
-      });
-
-      // Redis won't work without error handling.
-      client.on("error", () => {});
-    } catch (error) {
-      console.warn("Failed to create Redis client:", error);
-    }
-
-    if (client) {
-      try {
-        // Wait for the client to connect with a timeout of 5000ms.
-        const connectPromise = client.connect();
-        const timeoutPromise = createTimeoutPromise(5000, "Redis connection timed out"); // 5000ms timeout
-        await Promise.race([connectPromise, timeoutPromise]);
-      } catch (error) {
-        console.warn("Failed to connect Redis client:", error);
-
-        console.warn("Disconnecting the Redis client...");
-        // Try to disconnect the client to stop it from reconnecting.
-        client
-          .disconnect()
-          .then(() => {
-            console.info("Redis client disconnected.");
-          })
-          .catch(() => {
-            console.warn("Failed to quit the Redis client after failing to connect.");
-          });
-      }
-    }
-  }
-
-  /** @type {import("@neshca/cache-handler").Handler | null} */
-  let handler;
-
-  if (client?.isReady) {
-    const redisHandlerOptions = {
-      client,
-      keyPrefix: "fb:",
-      timeoutMs: 1000,
-    };
-
-    // Create the `redis-stack` Handler if the client is available and connected.
-    handler = await createRedisHandler(redisHandlerOptions);
-  } else {
-    // Fallback to LRU handler if Redis client is not available.
-    // The application will still work, but the cache will be in memory only and not shared.
-    handler = createLruHandler();
-    console.log("Using LRU handler for caching.");
-  }
-
-  return {
-    handlers: [handler],
-    ttl: {
-      // We set the stale and the expire age to the same value, because the stale age is determined by the unstable_cache revalidation.
-      defaultStaleAge: (process.env.REDIS_URL && Number(process.env.REDIS_DEFAULT_TTL)) || 86400,
-      estimateExpireAge: (staleAge) => staleAge,
-    },
-  };
-});
-
-export default CacheHandler;
diff --git a/apps/web/lib/constants.ts b/apps/web/lib/constants.ts
index ecae3f237336..7c1877f808a2 100644
--- a/apps/web/lib/constants.ts
+++ b/apps/web/lib/constants.ts
@@ -205,7 +205,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 REDIS_DEFAULT_TTL = env.REDIS_DEFAULT_TTL;
 export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1";
 export const UNKEY_ROOT_KEY = env.UNKEY_ROOT_KEY;
 
diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts
index 42428335b10e..c7dc1fd3af6c 100644
--- a/apps/web/lib/env.ts
+++ b/apps/web/lib/env.ts
@@ -56,7 +56,6 @@ export const env = createEnv({
     OIDC_SIGNING_ALGORITHM: z.string().optional(),
     OPENTELEMETRY_LISTENER_URL: z.string().optional(),
     REDIS_URL: z.string().optional(),
-    REDIS_DEFAULT_TTL: z.string().optional(),
     REDIS_HTTP_URL: z.string().optional(),
     PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
     POSTHOG_API_HOST: z.string().optional(),
@@ -163,7 +162,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_DEFAULT_TTL: process.env.REDIS_DEFAULT_TTL,
     REDIS_HTTP_URL: process.env.REDIS_HTTP_URL,
     PASSWORD_RESET_DISABLED: process.env.PASSWORD_RESET_DISABLED,
     PRIVACY_URL: process.env.PRIVACY_URL,
diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs
index 85aa8c9acb1a..ddc3bfc9df96 100644
--- a/apps/web/next.config.mjs
+++ b/apps/web/next.config.mjs
@@ -16,6 +16,8 @@ const getHostname = (url) => {
 
 const nextConfig = {
   assetPrefix: process.env.ASSET_PREFIX_URL || undefined,
+  cacheHandler: require.resolve("./cache-handler.js"),
+  cacheMaxMemorySize: 0, // disable default in-memory caching
   output: "standalone",
   poweredByHeader: false,
   productionBrowserSourceMaps: false,
@@ -282,11 +284,6 @@ const nextConfig = {
   },
 };
 
-// set custom cache handler
-if (process.env.CUSTOM_CACHE_DISABLED !== "1") {
-  nextConfig.cacheHandler = require.resolve("./cache-handler.mjs");
-}
-
 // set actions allowed origins
 if (process.env.WEBAPP_URL) {
   nextConfig.experimental.serverActions = {
@@ -302,22 +299,25 @@ nextConfig.images.remotePatterns.push({
 });
 
 const sentryOptions = {
-// For all available options, see:
-// https://www.npmjs.com/package/@sentry/webpack-plugin#options
+  // For all available options, see:
+  // https://www.npmjs.com/package/@sentry/webpack-plugin#options
 
-org: "formbricks",
-project: "formbricks-cloud",
+  org: "formbricks",
+  project: "formbricks-cloud",
 
-// Only print logs for uploading source maps in CI
-silent: true,
+  // Only print logs for uploading source maps in CI
+  silent: true,
 
-// Upload a larger set of source maps for prettier stack traces (increases build time)
-widenClientFileUpload: true,
+  // Upload a larger set of source maps for prettier stack traces (increases build time)
+  widenClientFileUpload: true,
 
-// Automatically tree-shake Sentry logger statements to reduce bundle size
-disableLogger: true,
+  // Automatically tree-shake Sentry logger statements to reduce bundle size
+  disableLogger: true,
 };
 
-const exportConfig = (process.env.SENTRY_DSN && process.env.NODE_ENV === "production") ? withSentryConfig(nextConfig, sentryOptions) : nextConfig;
+const exportConfig =
+  process.env.SENTRY_DSN && process.env.NODE_ENV === "production"
+    ? withSentryConfig(nextConfig, sentryOptions)
+    : nextConfig;
 
 export default exportConfig;
diff --git a/apps/web/package.json b/apps/web/package.json
index 3ed9ef60dd41..5d1d7a6796a1 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -32,6 +32,7 @@
     "@formbricks/logger": "workspace:*",
     "@formbricks/surveys": "workspace:*",
     "@formbricks/types": "workspace:*",
+    "@fortedigital/nextjs-cache-handler": "1.2.0",
     "@hookform/resolvers": "5.0.1",
     "@intercom/messenger-js-sdk": "0.0.14",
     "@json2csv/node": "7.0.6",
@@ -111,8 +112,8 @@
     "posthog-js": "1.240.0",
     "posthog-node": "4.17.1",
     "prismjs": "1.30.0",
-    "qrcode": "1.5.4",
     "qr-code-styling": "1.9.2",
+    "qrcode": "1.5.4",
     "react": "19.1.0",
     "react-colorful": "5.6.1",
     "react-confetti": "6.4.0",
@@ -158,8 +159,8 @@
     "autoprefixer": "10.4.21",
     "dotenv": "16.5.0",
     "postcss": "8.5.3",
-    "ts-node": "10.9.2",
     "resize-observer-polyfill": "1.5.1",
+    "ts-node": "10.9.2",
     "vite": "6.3.5",
     "vite-tsconfig-paths": "5.1.4",
     "vitest": "3.1.3",
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 3cb2e407b5ab..163cf4a68f21 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -16,13 +16,13 @@ services:
       - 8025:8025 # web ui
       - 1025:1025 # smtp server
 
-  redis:
-    image: redis:7.0.11
-    command: "redis-server"
+  valkey:
+    image: valkey/valkey:8.1.1
+    command: "valkey-server"
     ports:
       - 6379:6379
     volumes:
-      - redis-data:/data
+      - valkey-data:/data
 
   minio:
     image: minio/minio:RELEASE.2025-02-28T09-55-16Z
@@ -31,15 +31,15 @@ services:
       - MINIO_ROOT_USER=devminio
       - MINIO_ROOT_PASSWORD=devminio123
     ports:
-      - "9000:9000"  # S3 API
-      - "9001:9001"  # Console
+      - "9000:9000" # S3 API
+      - "9001:9001" # Console
     volumes:
       - minio-data:/data
 
 volumes:
   postgres:
     driver: local
-  redis-data:
+  valkey-data:
     driver: local
   minio-data:
     driver: local
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 6d67334e6585..6c19162bf3af 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -175,7 +175,6 @@ x-environment: &environment
 
     # Set the below to use Redis for Next Caching (default is In-Memory from Next Cache)
     # REDIS_URL:
-    # REDIS_DEFAULT_TTL:
 
     # Set the below to use for Rate Limiting (default us In-Memory LRU Cache)
     # REDIS_HTTP_URL:
diff --git a/docs/self-hosting/configuration/environment-variables.mdx b/docs/self-hosting/configuration/environment-variables.mdx
index bbd6517b6f20..a2e62b037efe 100644
--- a/docs/self-hosting/configuration/environment-variables.mdx
+++ b/docs/self-hosting/configuration/environment-variables.mdx
@@ -63,7 +63,6 @@ These variables are present inside your machine's docker-compose file. Restart t
 | 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                                                |                                                                           |
-| CUSTOM_CACHE_DISABLED        | Disables custom cache handler if set to 1 (required for deployment on Vercel)                                  | 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                                                                         |
diff --git a/docs/self-hosting/setup/cluster-setup.mdx b/docs/self-hosting/setup/cluster-setup.mdx
index 84d1f7e7453c..73d4b6244dc0 100644
--- a/docs/self-hosting/setup/cluster-setup.mdx
+++ b/docs/self-hosting/setup/cluster-setup.mdx
@@ -126,7 +126,6 @@ Configure Redis by adding the following environment variables to your instances:
 
 ```sh env
 REDIS_URL=redis://your-redis-host:6379
-REDIS_DEFAULT_TTL=86400
 REDIS_HTTP_URL=http://your-redis-host:8000
 ```
 
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b3cf8a7c5d73..e7f5ce688698 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -151,6 +151,9 @@ importers:
       '@formbricks/types':
         specifier: workspace:*
         version: link:../../packages/types
+      '@fortedigital/nextjs-cache-handler':
+        specifier: 1.2.0
+        version: 1.2.0(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(redis@4.7.0)
       '@hookform/resolvers':
         specifier: 5.0.1
         version: 5.0.1(react-hook-form@7.56.2(react@19.1.0))
@@ -1679,6 +1682,12 @@ packages:
   '@formkit/auto-animate@0.8.2':
     resolution: {integrity: sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ==}
 
+  '@fortedigital/nextjs-cache-handler@1.2.0':
+    resolution: {integrity: sha512-dHu7+D6yVHI5ii1/DgNSZM9wVPk8uKAB0zrRoNNbZq6hggpRRwAExV4J6bSGOd26RN6ZnfYaGLBmdb0gLpeBQg==}
+    peerDependencies:
+      next: '>=13.5.1'
+      redis: '>=4.6'
+
   '@gar/promisify@1.1.3':
     resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
 
@@ -11804,6 +11813,16 @@ snapshots:
 
   '@formkit/auto-animate@0.8.2': {}
 
+  '@fortedigital/nextjs-cache-handler@1.2.0(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(redis@4.7.0)':
+    dependencies:
+      '@neshca/cache-handler': 1.9.0(next@15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(redis@4.7.0)
+      cluster-key-slot: 1.1.2
+      lru-cache: 11.1.0
+      next: 15.3.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+      redis: 4.7.0
+    optionalDependencies:
+      '@rollup/rollup-linux-x64-gnu': 4.40.0
+
   '@gar/promisify@1.1.3':
     optional: true
 
diff --git a/turbo.json b/turbo.json
index e914df5c5438..c0b51c6477eb 100644
--- a/turbo.json
+++ b/turbo.json
@@ -77,7 +77,6 @@
         "BREVO_LIST_ID",
         "DOCKER_CRON_ENABLED",
         "CRON_SECRET",
-        "CUSTOM_CACHE_DISABLED",
         "DATABASE_URL",
         "DEBUG",
         "E2E_TESTING",
@@ -133,7 +132,6 @@
         "RATE_LIMITING_DISABLED",
         "REDIS_HTTP_URL",
         "REDIS_URL",
-        "REDIS_DEFAULT_TTL",
         "S3_ACCESS_KEY",
         "S3_BUCKET_NAME",
         "S3_ENDPOINT_URL",


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR fixes the Redis cache functionality for Next.js 15 by replacing the custom cache handler with the @fortedigital/nextjs-cache-handler library and removing the deprecated CUSTOM_CACHE_DISABLED environment variable.
- Removed references to CUSTOM_CACHE_DISABLED from configuration and documentation.
- Updated dependency management to include @fortedigital/nextjs-cache-handler (v1.2.0) and adjusted cache handling in apps/web/cache-handler.mjs.
- Refactored apps/web/next.config.mjs to use the new cache handler by default and applied improved formatting.

### Reviewed Changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                                       | Description                                                        |
| ---------------------------------------------------------- | ------------------------------------------------------------------ |
| turbo.json                                                 | Removed deprecated CUSTOM_CACHE_DISABLED from the environment list.|
| docs/self-hosting/configuration/environment-variables.mdx  | Removed outdated documentation for CUSTOM_CACHE_DISABLED.          |
| apps/web/package.json                                      | Added new dependency for @fortedigital/nextjs-cache-handler.         |
| apps/web/next.config.mjs                                   | Eliminated conditional logic for the custom cache and improved formatting. |
| apps/web/cache-handler.mjs                                 | Replaced custom cache handler logic with a composite handler strategy.|
| .env.example                                               | Removed comments referencing the custom cache handler variable.      |
</details>


<details>
<summary>Files not reviewed (1)</summary>

* **pnpm-lock.yaml**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**apps/web/cache-handler.mjs:23**
* [nitpick] Using global variables for caching configuration may lead to unexpected behavior in concurrent environments; consider namespacing these globals or adding explicit synchronization to ensure consistent state.
```
if (global.cacheHandlerConfig) {
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:34:55Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>apps/web/next.config.mjs (1)</summary><blockquote>

`18-21`: **Path resolution works, but consider `import.meta.url` for consistency**

`require.resolve("./cache-handler.mjs")` is evaluated via a CommonJS `require` created earlier.  
It resolves relative to the current file, which is fine, but the rest of this ESM config already relies on `fileURLToPath(import.meta.url)`.  
For consistency – and to avoid edge-cases when the config is moved – you could do

```diff
-  cacheHandler: require.resolve("./cache-handler.mjs"),
+  cacheHandler: fileURLToPath(new URL("./cache-handler.mjs", import.meta.url)),
```

Not mandatory, but it keeps everything on the same resolution strategy.

</blockquote></details>
<details>
<summary>apps/web/cache-handler.mjs (2)</summary><blockquote>

`40-61`: **Verify Redis initialisation & option usage**

1. `pingInterval` is not a recognised option in redis 4.x `createClient`. It will be ignored.  
   If you intended keep-alive pings, consider `socket: { keepAlive: 10000 }` instead.

2. The guard `PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE` means Redis _won’t_ be initialised during `next build`, but _will_ at runtime. That’s usually OK; just double-check that server-side rendering in `next start` still sees `NEXT_PHASE` unset (it usually is).

No change required if this was deliberate, otherwise update the options block.

---

`20-37`: **Development shortcut is helpful – document it**

Early-returning an in-memory LRU cache in `NODE_ENV === "development"` is convenient, but production-like bugs can be masked when developers never hit Redis locally.

Consider:

```diff
   if (process.env.NODE_ENV === "development") {
-    const lruCache = createLruHandler();
-    return { handlers: [lruCache] };
+    const lruCache = createLruHandler();
+    console.info(
+      "[cache-handler] Using in-memory LRU cache because NODE_ENV=development; set USE_REDIS_LOCALLY=1 to test Redis."
+    );
+    return { handlers: [lruCache] };
   }
```

…and gate the early-return behind an env-flag (`USE_REDIS_LOCALLY`) so developers can opt-in to real Redis when debugging cache behaviour.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d1cdf6e21656a243ef023336cad3144da29fbec8 and 9dfb0d9d1c2b2cccbac9aac0c734ba54bed3e659.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `.env.example` (0 hunks)
* `apps/web/cache-handler.mjs` (1 hunks)
* `apps/web/next.config.mjs` (2 hunks)
* `apps/web/package.json` (3 hunks)
* `docs/self-hosting/configuration/environment-variables.mdx` (0 hunks)
* `turbo.json` (0 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (3)</summary>

* turbo.json
* .env.example
* docs/self-hosting/configuration/environment-variables.mdx

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: Run Unit Tests / Unit Tests
* GitHub Check: Build Formbricks-web / Build Formbricks-web
* GitHub Check: Run E2E Tests / Run E2E Tests
* GitHub Check: Validate Docker Build
* GitHub Check: SonarQube

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>apps/web/package.json (1)</summary>

`35-35`: **Confirm library version & licence compliance**

`@fortedigital/nextjs-cache-handler` is pinned to an exact `1.2.0` release.  
Before merging, please verify:

1. No critical fixes have landed in a newer `1.x` release that we would miss by pinning hard.  
2. The licence (MIT) is compatible with Formbricks’ OSS/commercial distribution policy.

If both checks pass, feel free to keep the exact pin; otherwise switch to a caret-range (`^1.2.0`) and/or raise an issue to track upgrades.

</details>
<details>
<summary>apps/web/next.config.mjs (1)</summary>

`301-316`: **Comment-only change – no issues**  
These lines only rearrange Sentry comments/flags; runtime behaviour is unchanged.

</details>
<details>
<summary>apps/web/cache-handler.mjs (1)</summary>

`110-110`: **Export is fine**

Exporting a singleton `Next15CacheHandler` is the recommended pattern for Next 15; no issues spotted.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:43:25Z
user: login: mattinannt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:45:52Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T13:54:23Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>apps/web/cache-handler.js (2)</summary><blockquote>

`33-37`: **Development mode optimization**

Bypassing Redis in development mode is a good practice for local development. However, the comment "You may need to ignore Redis locally, remove this block otherwise" might confuse developers.

```diff
-  // You may need to ignore Redis locally, remove this block otherwise
+  // In development mode, we use only the LRU cache for simplicity
```

---

`62-73`: **Connection lifecycle management**

Good implementation of connection establishment with proper logging and error handling. However, consider adding event listeners for disconnection events.

```diff
 if (redisClient) {
   try {
     console.info("Connecting Redis client...");
     await redisClient.connect();
     console.info("Redis client connected.");
+    
+    // Add event listeners for reconnection attempts
+    redisClient.on("reconnecting", () => {
+      console.info("Redis client attempting to reconnect...");
+    });
   } catch (error) {
     console.error("Failed to connect Redis client:", error);
     await redisClient
       .disconnect()
       .catch(() => console.error("Failed to quit the Redis client after failing to connect."));
   }
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9dfb0d9d1c2b2cccbac9aac0c734ba54bed3e659 and 5997cd7a9c5bd8616c7ffb90cf22d595bad63172.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `apps/web/cache-handler.js` (1 hunks)
* `apps/web/next.config.mjs` (2 hunks)
* `docker-compose.dev.yml` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* apps/web/next.config.mjs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Run E2E Tests / Run E2E Tests
* GitHub Check: Validate Docker Build

</details>

<details>
<summary>🔇 Additional comments (11)</summary><blockquote>

<details>
<summary>apps/web/cache-handler.js (8)</summary>

`1-3`: **Good documentation reference!**

Clear reference to the source example and documentation, which makes it easier for developers to understand where this implementation comes from and find additional information.

---

`17-18`: **Appropriate use of Next15CacheHandler**

Using Next15CacheHandler from @fortedigital/nextjs-cache-handler is the right approach for Next.js 15 compatibility, as it handles the buffer/string requirements needed for the newer version.

---

`19-32`: **Effective singleton pattern implementation**

The global state management prevents race conditions and duplicate Redis connections - this is a critical pattern for production stability. The double-check for both `cacheHandlerConfig` and `cacheHandlerConfigPromise` properly handles concurrent initialization scenarios.

---

`50-60`: **Robust Redis client error handling**

The error handling for Redis client creation is thorough, with appropriate error logging and cleanup of global configuration objects on error.

---

`74-81`: **Proper fallback strategy**

Excellent fallback to LRU cache when Redis is unavailable, ensuring the application can still function with degraded caching performance.

---

`83-87`: **Redis cache configuration**

The Redis handler is properly configured with a prefix to avoid key collisions with other services using the same Redis instance.

---

`90-105`: **Well-designed composite caching strategy**

The composite handler implementation elegantly switches between LRU and Redis based on cache tags. This allows for fine-grained control over which cache to use for specific data.

---

`110-111`: **Export of Next15CacheHandler**

The module correctly exports a new instance of Next15CacheHandler, which implements the necessary interfaces for Next.js 15 compatibility.

</details>
<details>
<summary>docker-compose.dev.yml (3)</summary>

`34-35`: **Minor formatting improvement**

Uniform formatting of port comments improves readability.

---

`42-42`: **Volume name updated for consistency**

The volume name has been appropriately updated from `redis-data` to `valkey-data` to match the service rename.

---

`19-25`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify Valkey compatibility with Redis client**

Valkey is replacing Redis in the development environment. While Valkey is likely Redis API-compatible, verify that the cache handler will work seamlessly with it.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check Valkey compatibility documentation
echo "Checking Valkey compatibility with Redis clients..."
curl -s https://api.github.com/repos/valkey-io/valkey/releases/latest | grep -o '"name": "[^"]*"' | head -n 1
curl -s https://raw.githubusercontent.com/valkey-io/valkey/master/README.md | grep -i "redis compatible" -A 5
```

Length of output: 352

---



</details>

**Verify Valkey’s Redis API Compatibility**

File: docker-compose.dev.yml  
Lines: 19–25

```
  valkey:
    image: valkey/valkey:8.1.1
    command: "valkey-server"
    ports:
      - 6379:6379
    volumes:
      - valkey-data:/data
```

Valkey is replacing Redis in development, but its public docs for v8.1.1 don’t explicitly confirm full Redis API compatibility. Please manually verify that our cache handler works seamlessly by:

• Connecting with a standard Redis client (e.g., `redis-cli -h valkey -p 6379`) and running basic commands:  
  ```bash
  redis-cli -h valkey -p 6379 PING
  redis-cli -h valkey -p 6379 SET test-key "value"
  redis-cli -h valkey -p 6379 GET test-key
  ```  
• Reviewing the official Valkey compatibility guide in their documentation or GitHub repo.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T14:01:13Z
user: login: jobenjada
body: 🤞
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T15:23:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>apps/web/cache-handler.js (1)</summary><blockquote>

`93-94`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify the composition of caching strategies**

The file exports a `Next15CacheHandler` instance, but it's not clear how the LRU cache and Redis cache are composed together in the exported handler. According to the PR objectives, this should implement a composite approach combining both strategies.


Run this script to understand how the handler uses both caching strategies:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how Next15CacheHandler works with multiple handlers
grep -r "Next15CacheHandler" --include="*.js" --include="*.ts" node_modules/@fortedigital/nextjs-cache-handler
```

Length of output: 191

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate definition and usage of Next15CacheHandler in the codebase
grep -R "class Next15CacheHandler" -n .
grep -R "new Next15CacheHandler" -n .
```

Length of output: 253

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the import and setup of Next15CacheHandler in cache-handler.js
sed -n '1,50p' apps/web/cache-handler.js
```

Length of output: 2100

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate and inspect Next15CacheHandler implementation in the dependency
find node_modules/@fortedigital/nextjs-cache-handler -type f -name "next-15-cache-handler*.js" -o -name "next-15-cache-handler*.ts" | xargs sed -n '1,200p'
```

Length of output: 238

---

🏁 Script executed:

```shell
#!/bin/bash
# Find where Redis handler is created and how handlers are composed
grep -R "createRedisHandler" -n apps/web/cache-handler.js
grep -R "handlers" -n apps/web/cache-handler.js
sed -n '50,200p' apps/web/cache-handler.js
```

Length of output: 1619

---



</details>

**Ensure Redis and LRU handlers are actually composed**

In the “redis enabled” path you call `createRedisHandler(...)` but never include its return value in your `handlers` array (and you don’t set `global.cacheHandlerConfig` before returning). You need to capture the Redis handler and return both strategies together. For example, in apps/web/cache-handler.js around lines 80–95:

```diff
     const lruCache = createLruHandler();

-    if (!redisClient?.isReady) {
+    if (!redisClient?.isReady) {
       console.error("Failed to initialize caching layer.");
       global.cacheHandlerConfigPromise = null;
       global.cacheHandlerConfig = { handlers: [lruCache] };
       return global.cacheHandlerConfig;
     }

-    createRedisHandler({
-      client: redisClient,
-      keyPrefix: "nextjs:",
-    });
-
-    global.cacheHandlerConfigPromise = null;
-
-    return global.cacheHandlerConfig;
+    // compose both LRU and Redis caches
+    const redisCache = createRedisHandler({
+      client: redisClient,
+      keyPrefix: "nextjs:",
+    });
+    global.cacheHandlerConfig = {
+      handlers: [lruCache, redisCache],
+    };
+    global.cacheHandlerConfigPromise = null;
+    return global.cacheHandlerConfig;
```

• Capture and include the return value of `createRedisHandler`  
• Assign `global.cacheHandlerConfig` to `{ handlers: [lruCache, redisCache] }` before returning  
• Clear `cacheHandlerConfigPromise` as you already do

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>apps/web/cache-handler.js (1)</summary><blockquote>

`31-34`: _🛠️ Refactor suggestion_

**Add validation for REDIS_URL format**

While you check for the existence of `REDIS_URL`, there's no validation of its format. This can lead to silent failures if the URL is malformed.



```diff
// If REDIS_URL is not set, we will use LRU cache only
-if (!process.env.REDIS_URL) {
+const redisUrl = process.env.REDIS_URL;
+if (!redisUrl || !redisUrl.match(/^redis:\/\/|rediss:\/\//)) {
+  if (redisUrl && !redisUrl.match(/^redis:\/\/|rediss:\/\//)) {
+    console.warn("Invalid REDIS_URL format. Expected format: redis://user:password@host:port");
+  }
  const lruCache = createLruHandler();
  return { handlers: [lruCache] };
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>apps/web/cache-handler.js (1)</summary><blockquote>

`42-45`: **Consider making Redis connection settings configurable**

The Redis connection settings (especially the `pingInterval`) are hardcoded. Consider making these configurable through environment variables for more flexibility across different environments.


```diff
const settings = {
  url: process.env.REDIS_URL, // Make sure you configure this variable
-  pingInterval: 10000,
+  pingInterval: parseInt(process.env.REDIS_PING_INTERVAL || '10000', 10),
};
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 78326d8f8b4ccf25e34bb2c0bd9305a89da1affb and 43c71b3d66e74edc7017ea2b15eb29f9b3a8c304.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `apps/web/cache-handler.js` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>apps/web/cache-handler.js (6)</summary>

`1-15`: **Good job implementing the new cache handler for Next.js 15!**

The import structure is well-organized with clear separation between:
- `@neshca/cache-handler` core dependencies
- Next.js and Redis dependencies
- `@fortedigital/nextjs-cache-handler` specialized dependencies

This makes the dependencies and their purposes clear to anyone reading the code.

---

`17-34`: **Good pattern for singleton Redis connection management**

Using global scope to maintain a single Redis connection is an excellent practice to prevent connection leaks. The code correctly handles:
1. Returning existing configuration if available
2. Handling race conditions with a promise
3. Falling back to LRU cache when Redis isn't configured

This implementation follows the recommended practices from the `@neshca/cache-handler` documentation.

---

`47-57`: **Good error handling for Redis client creation**

The code properly handles potential errors during Redis client creation and sets up error event listeners to clear the global configuration when Redis encounters runtime errors. This ensures your application can gracefully handle intermittent Redis connection issues.

---

`59-70`: **Good Redis connection management**

The connection sequence is well-implemented with:
1. Proper logging of connection attempts
2. Error catching during connection
3. Clean disconnect attempts on failure

This ensures connections are properly established or cleaned up in case of failure.

---

`71-78`: **Good fallback mechanism to LRU cache**

The code correctly falls back to the in-memory LRU cache when Redis is not ready, providing a resilient caching solution that works even when Redis is unavailable.

---

`80-83`: <details>
<summary>❓ Verification inconclusive</summary>

**Clarify the Redis handler integration**

The `createRedisHandler` function is called, but its return value isn't stored or used directly in the handler configuration. It appears to be modifying the global state, but this isn't immediately obvious from the code.


Consider checking how the Redis handler integrates with the overall caching strategy:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if and how the Redis handler is used
grep -r "createRedisHandler" --include="*.js" --include="*.ts" node_modules/@fortedigital/nextjs-cache-handler
```

Length of output: 191

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the cache-handler file
fd cache-handler.js

# Search for all usages/definitions of createRedisHandler in the repo
rg "createRedisHandler" -n .
```

Length of output: 259

---



</details>

**Clarify Redis handler integration in apps/web/cache-handler.js**  

The call to `createRedisHandler({ client: redisClient, keyPrefix: "nextjs:" })` on lines 80–83 isn’t capturing or using its return value. It’s unclear whether:

- `createRedisHandler` returns a handler function that must be assigned (e.g.  
  `const redisHandler = createRedisHandler(...);` and then used in your middleware/route), or  
- It relies solely on side-effects (registering itself globally).

Please verify `createRedisHandler`’s implementation and adjust one of the following:

- If it returns a function, store and hook it into your caching pipeline.  
- If it only has side-effects, add a brief comment or documentation to make that clear.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:23:44Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>apps/web/cache-handler.js (1)</summary><blockquote>

`30-34`: _🛠️ Refactor suggestion_

**Add validation for REDIS_URL format.**

While the code checks if REDIS_URL is set, it doesn't validate the URL format. If REDIS_URL exists but is malformed, this could lead to connection failures without clear error messages.



Add URL validation before proceeding with Redis client creation:

```diff
// If REDIS_URL is not set, we will use LRU cache only
- if (!process.env.REDIS_URL) {
+ const redisUrl = process.env.REDIS_URL;
+ if (!redisUrl) {
   const lruCache = createLruHandler();
   return { handlers: [lruCache] };
 }
+ 
+ // Simple URL validation to catch obvious formatting issues
+ try {
+   new URL(redisUrl);
+ } catch (error) {
+   console.error("Invalid REDIS_URL format:", error.message);
+   const lruCache = createLruHandler();
+   return { handlers: [lruCache] };
+ }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>apps/web/cache-handler.js (3)</summary><blockquote>

`37-70`: **Enhance Redis error handling and connection management.**

The error handling for Redis is comprehensive, but consider these improvements:

1. Add a connection timeout to prevent hanging if Redis is unreachable
2. Implement an explicit reconnection strategy beyond the pingInterval
3. Extract the disconnection logic to a reusable function


```diff
try {
  redisClient = createClient(settings);
  redisClient.on("error", (e) => {
    console.error("Redis error", e);
    global.cacheHandlerConfig = null;
    global.cacheHandlerConfigPromise = null;
  });
+ 
+  // Add reconnection strategy
+  redisClient.on("reconnecting", () => {
+    console.info("Attempting to reconnect to Redis...");
+  });
} catch (error) {
  console.error("Failed to create Redis client:", error);
}

if (redisClient) {
  try {
    console.info("Connecting Redis client...");
-   await redisClient.connect();
+   // Add timeout to connection attempt
+   await Promise.race([
+     redisClient.connect(),
+     new Promise((_, reject) => 
+       setTimeout(() => reject(new Error("Redis connection timeout")), 5000)
+     )
+   ]);
    console.info("Redis client connected.");
  } catch (error) {
    console.error("Failed to connect Redis client:", error);
    await redisClient
      .disconnect()
      .catch(() => console.error("Failed to quit the Redis client after failing to connect."));
  }
}
```

---

`80-83`: **Consider configurable Redis key prefix.**

The Redis key prefix is hardcoded as "nextjs:". Consider making this configurable through an environment variable to allow for multiple applications sharing the same Redis instance.


```diff
const redisCacheHandler = createRedisHandler({
  client: redisClient,
-  keyPrefix: "nextjs:",
+  keyPrefix: process.env.REDIS_KEY_PREFIX || "nextjs:",
});
```

---

`97-97`: **Exported handler lacks customization options.**

The exported Next15CacheHandler is instantiated without any configuration options. Consider allowing for runtime configuration by exporting a factory function instead of a singleton instance.


```diff
-module.exports = new Next15CacheHandler();
+const createCacheHandler = (options = {}) => new Next15CacheHandler(options);
+module.exports = createCacheHandler();
+module.exports.createCacheHandler = createCacheHandler;
```

This would enable more flexible usage while maintaining backward compatibility.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1b667bc62925156ed284c5f67416283a805578db and 33634dd17fb28b84b13d99eee1559fa397aa6f58.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `apps/web/cache-handler.js` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: Run Unit Tests / Unit Tests
* GitHub Check: Run E2E Tests / Run E2E Tests
* GitHub Check: Build Formbricks-web / Build Formbricks-web
* GitHub Check: SonarQube
* GitHub Check: Validate Docker Build

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>apps/web/cache-handler.js (2)</summary>

`1-15`: **Well-organized import structure with clear documentation.**

The import structure is clean with good comments that group related dependencies and explain the purpose of each section. The reference to the original implementation example is helpful for future maintainers.

---

`17-28`: **Good implementation of singleton pattern for Redis connection.**

The code correctly implements a singleton pattern using global scope to prevent multiple Redis connections, which is important for performance. The race condition handling with separate variables for the config and its promise is well thought out.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:01:03Z

Review comments:
diff_hunk: @@ -1,79 +1,110 @@
-import { CacheHandler } from "@neshca/cache-handler";
-import createLruHandler from "@neshca/cache-handler/local-lru";
-import createRedisHandler from "@neshca/cache-handler/redis-strings";
-import { createClient } from "redis";
-
-// Function to create a timeout promise
-const createTimeoutPromise = (ms, rejectReason) => {
-  return new Promise((_, reject) => setTimeout(() => reject(new Error(rejectReason)), ms));
-};
-
-CacheHandler.onCreation(async () => {
-  let client;
-
-  if (process.env.REDIS_URL) {
-    try {
-      // Create a Redis client.
-      client = createClient({
-        url: process.env.REDIS_URL,
-      });
-
-      // Redis won't work without error handling.
-      client.on("error", () => {});
-    } catch (error) {
-      console.warn("Failed to create Redis client:", error);
-    }
+// This cache handler follows the @fortedigital/nextjs-cache-handler example
+// Read more at: https://github.com/fortedigital/nextjs-cache-handler
+
+// @neshca/cache-handler dependencies
+const { CacheHandler } = require("@neshca/cache-handler");
+const createLruHandler = require("@neshca/cache-handler/local-lru").default;
+
+// Next/Redis dependencies
+const { createClient } = require("redis");
+const { PHASE_PRODUCTION_BUILD } = require("next/constants");
+
+// @fortedigital/nextjs-cache-handler dependencies
+const createCompositeHandler = require("@fortedigital/nextjs-cache-handler/composite").default;
+const createRedisHandler = require("@fortedigital/nextjs-cache-handler/redis-strings").default;
+const createBufferStringHandler =
+  require("@fortedigital/nextjs-cache-handler/buffer-string-decorator").default;
+const { Next15CacheHandler } = require("@fortedigital/nextjs-cache-handler/next-15-cache-handler");
+
+// Usual onCreation from @neshca/cache-handler
+CacheHandler.onCreation(() => {
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures only one instance get created
+  if (global.cacheHandlerConfig) {
+    return global.cacheHandlerConfig;
+  }
+
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures new instances are not created in a race condition
+  if (global.cacheHandlerConfigPromise) {
+    return global.cacheHandlerConfigPromise;
+  }
+
+  // You may need to ignore Redis locally, remove this block otherwise
+  if (process.env.NODE_ENV === "development") {
+    const lruCache = createLruHandler();
+    return { handlers: [lruCache] };
+  }
+
+  // Main promise initializing the handler
+  global.cacheHandlerConfigPromise = (async () => {
+    /** @type {import("redis").RedisClientType | null} */
+    let redisClient = null;
+    // eslint-disable-next-line turbo/no-undeclared-env-vars -- Next.js will inject this variable
+    if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
+      const settings = {
+        url: process.env.REDIS_URL, // Make sure you configure this variable
+        pingInterval: 10000,
+      };
 
-    if (client) {
       try {
-        // Wait for the client to connect with a timeout of 5000ms.
-        const connectPromise = client.connect();
-        const timeoutPromise = createTimeoutPromise(5000, "Redis connection timed out"); // 5000ms timeout
-        await Promise.race([connectPromise, timeoutPromise]);
+        redisClient = createClient(settings);
+        redisClient.on("error", (e) => {
+          console.error("Redis error", e);
+          global.cacheHandlerConfig = null;
+          global.cacheHandlerConfigPromise = null;
+        });
       } catch (error) {
-        console.warn("Failed to connect Redis client:", error);
+        console.error("Failed to create Redis client:", error);
+      }
+    }
 
-        console.warn("Disconnecting the Redis client...");
-        // Try to disconnect the client to stop it from reconnecting.
-        client
+    if (redisClient) {
+      try {
+        console.info("Connecting Redis client...");
+        await redisClient.connect();
+        console.info("Redis client connected.");
+      } catch (error) {
+        console.error("Failed to connect Redis client:", error);
+        await redisClient
           .disconnect()
-          .then(() => {
-            console.info("Redis client disconnected.");
-          })
-          .catch(() => {
-            console.warn("Failed to quit the Redis client after failing to connect.");
-          });
+          .catch(() => console.error("Failed to quit the Redis client after failing to connect."));
       }
     }
-  }
+    const lruCache = createLruHandler();
+
+    if (!redisClient?.isReady) {
+      console.error("Failed to initialize caching layer.");
+      global.cacheHandlerConfigPromise = null;
+      global.cacheHandlerConfig = { handlers: [lruCache] };
+      return global.cacheHandlerConfig;
+    }
 
-  /** @type {import("@neshca/cache-handler").Handler | null} */
-  let handler;
+    const redisCacheHandler = createRedisHandler({
+      client: redisClient,
+      keyPrefix: "nextjs:",
+    });
 
-  if (client?.isReady) {
-    const redisHandlerOptions = {
-      client,
-      keyPrefix: "fb:",
-      timeoutMs: 1000,
+    global.cacheHandlerConfigPromise = null;
+
+    // This example uses composite handler to switch from Redis to LRU cache if tags contains `memory-cache` tag.
+    // You can skip composite and use Redis or LRU only.
+    global.cacheHandlerConfig = {
+      handlers: [
+        createCompositeHandler({
+          handlers: [
+            lruCache,
+            createBufferStringHandler(redisCacheHandler), // Use `createBufferStringHandler` in Next15 and ignore it in Next14 or below
+          ],
+          setStrategy: (ctx) => (ctx?.tags.includes("memory-cache") ? 0 : 1), // You can adjust strategy for deciding which cache should the composite use
+        }),
path: apps/web/cache-handler.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Potential runtime crash: optional-chaining stops too early**

`ctx?.tags.includes("memory-cache")` will throw when `ctx` exists but `ctx.tags` is `undefined`, because `.includes` is executed on `undefined`.

```diff
-          setStrategy: (ctx) => (ctx?.tags.includes("memory-cache") ? 0 : 1),
+          setStrategy: (ctx) => (ctx?.tags?.includes("memory-cache") ? 0 : 1),
```

Add the second `?.` so the call short-circuits safely.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            createBufferStringHandler(redisCacheHandler), // Use `createBufferStringHandler` in Next15 and ignore it in Next14 or below
          ],
-          setStrategy: (ctx) => (ctx?.tags.includes("memory-cache") ? 0 : 1), // You can adjust strategy for deciding which cache should the composite use
+          setStrategy: (ctx) => (ctx?.tags?.includes("memory-cache") ? 0 : 1), // You can adjust strategy for deciding which cache should the composite use
        }),
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/web/cache-handler.mjs around lines 97 to 100, the expression
ctx?.tags.includes("memory-cache") can cause a runtime error if ctx exists but
ctx.tags is undefined, since includes is called on undefined. To fix this, add
optional chaining before includes, changing it to
ctx?.tags?.includes("memory-cache") so the call short-circuits safely when tags
is undefined.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-08T13:43:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,79 +1,110 @@
-import { CacheHandler } from "@neshca/cache-handler";
-import createLruHandler from "@neshca/cache-handler/local-lru";
-import createRedisHandler from "@neshca/cache-handler/redis-strings";
-import { createClient } from "redis";
-
-// Function to create a timeout promise
-const createTimeoutPromise = (ms, rejectReason) => {
-  return new Promise((_, reject) => setTimeout(() => reject(new Error(rejectReason)), ms));
-};
-
-CacheHandler.onCreation(async () => {
-  let client;
-
-  if (process.env.REDIS_URL) {
-    try {
-      // Create a Redis client.
-      client = createClient({
-        url: process.env.REDIS_URL,
-      });
-
-      // Redis won't work without error handling.
-      client.on("error", () => {});
-    } catch (error) {
-      console.warn("Failed to create Redis client:", error);
-    }
+// This cache handler follows the @fortedigital/nextjs-cache-handler example
+// Read more at: https://github.com/fortedigital/nextjs-cache-handler
+
+// @neshca/cache-handler dependencies
+const { CacheHandler } = require("@neshca/cache-handler");
+const createLruHandler = require("@neshca/cache-handler/local-lru").default;
+
+// Next/Redis dependencies
+const { createClient } = require("redis");
+const { PHASE_PRODUCTION_BUILD } = require("next/constants");
+
+// @fortedigital/nextjs-cache-handler dependencies
+const createCompositeHandler = require("@fortedigital/nextjs-cache-handler/composite").default;
+const createRedisHandler = require("@fortedigital/nextjs-cache-handler/redis-strings").default;
+const createBufferStringHandler =
+  require("@fortedigital/nextjs-cache-handler/buffer-string-decorator").default;
+const { Next15CacheHandler } = require("@fortedigital/nextjs-cache-handler/next-15-cache-handler");
+
+// Usual onCreation from @neshca/cache-handler
+CacheHandler.onCreation(() => {
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures only one instance get created
+  if (global.cacheHandlerConfig) {
+    return global.cacheHandlerConfig;
+  }
+
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures new instances are not created in a race condition
+  if (global.cacheHandlerConfigPromise) {
+    return global.cacheHandlerConfigPromise;
+  }
+
+  // You may need to ignore Redis locally, remove this block otherwise
+  if (process.env.NODE_ENV === "development") {
+    const lruCache = createLruHandler();
+    return { handlers: [lruCache] };
+  }
+
+  // Main promise initializing the handler
+  global.cacheHandlerConfigPromise = (async () => {
+    /** @type {import("redis").RedisClientType | null} */
+    let redisClient = null;
+    // eslint-disable-next-line turbo/no-undeclared-env-vars -- Next.js will inject this variable
+    if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
+      const settings = {
+        url: process.env.REDIS_URL, // Make sure you configure this variable
+        pingInterval: 10000,
+      };
 
-    if (client) {
       try {
-        // Wait for the client to connect with a timeout of 5000ms.
-        const connectPromise = client.connect();
-        const timeoutPromise = createTimeoutPromise(5000, "Redis connection timed out"); // 5000ms timeout
-        await Promise.race([connectPromise, timeoutPromise]);
+        redisClient = createClient(settings);
+        redisClient.on("error", (e) => {
+          console.error("Redis error", e);
+          global.cacheHandlerConfig = null;
+          global.cacheHandlerConfigPromise = null;
+        });
       } catch (error) {
-        console.warn("Failed to connect Redis client:", error);
+        console.error("Failed to create Redis client:", error);
+      }
+    }
 
-        console.warn("Disconnecting the Redis client...");
-        // Try to disconnect the client to stop it from reconnecting.
-        client
+    if (redisClient) {
+      try {
+        console.info("Connecting Redis client...");
+        await redisClient.connect();
+        console.info("Redis client connected.");
+      } catch (error) {
+        console.error("Failed to connect Redis client:", error);
+        await redisClient
           .disconnect()
-          .then(() => {
-            console.info("Redis client disconnected.");
-          })
-          .catch(() => {
-            console.warn("Failed to quit the Redis client after failing to connect.");
-          });
+          .catch(() => console.error("Failed to quit the Redis client after failing to connect."));
       }
     }
-  }
+    const lruCache = createLruHandler();
+
+    if (!redisClient?.isReady) {
+      console.error("Failed to initialize caching layer.");
+      global.cacheHandlerConfigPromise = null;
+      global.cacheHandlerConfig = { handlers: [lruCache] };
+      return global.cacheHandlerConfig;
+    }
 
-  /** @type {import("@neshca/cache-handler").Handler | null} */
-  let handler;
+    const redisCacheHandler = createRedisHandler({
+      client: redisClient,
+      keyPrefix: "nextjs:",
+    });
 
-  if (client?.isReady) {
-    const redisHandlerOptions = {
-      client,
-      keyPrefix: "fb:",
-      timeoutMs: 1000,
+    global.cacheHandlerConfigPromise = null;
+
+    // This example uses composite handler to switch from Redis to LRU cache if tags contains `memory-cache` tag.
+    // You can skip composite and use Redis or LRU only.
+    global.cacheHandlerConfig = {
+      handlers: [
+        createCompositeHandler({
+          handlers: [
+            lruCache,
+            createBufferStringHandler(redisCacheHandler), // Use `createBufferStringHandler` in Next15 and ignore it in Next14 or below
+          ],
+          setStrategy: (ctx) => (ctx?.tags.includes("memory-cache") ? 0 : 1), // You can adjust strategy for deciding which cache should the composite use
+        }),
path: apps/web/cache-handler.mjs
user: login: mattinannt
body: fixed
created_at: 2025-05-08T13:45:52Z
author_association: MEMBER
diff_hunk: @@ -1,79 +1,110 @@
-import { CacheHandler } from "@neshca/cache-handler";
-import createLruHandler from "@neshca/cache-handler/local-lru";
-import createRedisHandler from "@neshca/cache-handler/redis-strings";
-import { createClient } from "redis";
-
-// Function to create a timeout promise
-const createTimeoutPromise = (ms, rejectReason) => {
-  return new Promise((_, reject) => setTimeout(() => reject(new Error(rejectReason)), ms));
-};
-
-CacheHandler.onCreation(async () => {
-  let client;
-
-  if (process.env.REDIS_URL) {
-    try {
-      // Create a Redis client.
-      client = createClient({
-        url: process.env.REDIS_URL,
-      });
-
-      // Redis won't work without error handling.
-      client.on("error", () => {});
-    } catch (error) {
-      console.warn("Failed to create Redis client:", error);
-    }
+// This cache handler follows the @fortedigital/nextjs-cache-handler example
+// Read more at: https://github.com/fortedigital/nextjs-cache-handler
+
+// @neshca/cache-handler dependencies
+const { CacheHandler } = require("@neshca/cache-handler");
+const createLruHandler = require("@neshca/cache-handler/local-lru").default;
+
+// Next/Redis dependencies
+const { createClient } = require("redis");
+const { PHASE_PRODUCTION_BUILD } = require("next/constants");
+
+// @fortedigital/nextjs-cache-handler dependencies
+const createCompositeHandler = require("@fortedigital/nextjs-cache-handler/composite").default;
+const createRedisHandler = require("@fortedigital/nextjs-cache-handler/redis-strings").default;
+const createBufferStringHandler =
+  require("@fortedigital/nextjs-cache-handler/buffer-string-decorator").default;
+const { Next15CacheHandler } = require("@fortedigital/nextjs-cache-handler/next-15-cache-handler");
+
+// Usual onCreation from @neshca/cache-handler
+CacheHandler.onCreation(() => {
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures only one instance get created
+  if (global.cacheHandlerConfig) {
+    return global.cacheHandlerConfig;
+  }
+
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures new instances are not created in a race condition
+  if (global.cacheHandlerConfigPromise) {
+    return global.cacheHandlerConfigPromise;
+  }
+
+  // You may need to ignore Redis locally, remove this block otherwise
+  if (process.env.NODE_ENV === "development") {
+    const lruCache = createLruHandler();
+    return { handlers: [lruCache] };
+  }
+
+  // Main promise initializing the handler
+  global.cacheHandlerConfigPromise = (async () => {
+    /** @type {import("redis").RedisClientType | null} */
+    let redisClient = null;
+    // eslint-disable-next-line turbo/no-undeclared-env-vars -- Next.js will inject this variable
+    if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
+      const settings = {
+        url: process.env.REDIS_URL, // Make sure you configure this variable
+        pingInterval: 10000,
+      };
 
-    if (client) {
       try {
-        // Wait for the client to connect with a timeout of 5000ms.
-        const connectPromise = client.connect();
-        const timeoutPromise = createTimeoutPromise(5000, "Redis connection timed out"); // 5000ms timeout
-        await Promise.race([connectPromise, timeoutPromise]);
+        redisClient = createClient(settings);
+        redisClient.on("error", (e) => {
+          console.error("Redis error", e);
+          global.cacheHandlerConfig = null;
+          global.cacheHandlerConfigPromise = null;
+        });
       } catch (error) {
-        console.warn("Failed to connect Redis client:", error);
+        console.error("Failed to create Redis client:", error);
+      }
+    }
 
-        console.warn("Disconnecting the Redis client...");
-        // Try to disconnect the client to stop it from reconnecting.
-        client
+    if (redisClient) {
+      try {
+        console.info("Connecting Redis client...");
+        await redisClient.connect();
+        console.info("Redis client connected.");
+      } catch (error) {
+        console.error("Failed to connect Redis client:", error);
+        await redisClient
           .disconnect()
-          .then(() => {
-            console.info("Redis client disconnected.");
-          })
-          .catch(() => {
-            console.warn("Failed to quit the Redis client after failing to connect.");
-          });
+          .catch(() => console.error("Failed to quit the Redis client after failing to connect."));
       }
     }
-  }
+    const lruCache = createLruHandler();
+
+    if (!redisClient?.isReady) {
+      console.error("Failed to initialize caching layer.");
+      global.cacheHandlerConfigPromise = null;
+      global.cacheHandlerConfig = { handlers: [lruCache] };
+      return global.cacheHandlerConfig;
+    }
 
-  /** @type {import("@neshca/cache-handler").Handler | null} */
-  let handler;
+    const redisCacheHandler = createRedisHandler({
+      client: redisClient,
+      keyPrefix: "nextjs:",
+    });
 
-  if (client?.isReady) {
-    const redisHandlerOptions = {
-      client,
-      keyPrefix: "fb:",
-      timeoutMs: 1000,
+    global.cacheHandlerConfigPromise = null;
+
+    // This example uses composite handler to switch from Redis to LRU cache if tags contains `memory-cache` tag.
+    // You can skip composite and use Redis or LRU only.
+    global.cacheHandlerConfig = {
+      handlers: [
+        createCompositeHandler({
+          handlers: [
+            lruCache,
+            createBufferStringHandler(redisCacheHandler), // Use `createBufferStringHandler` in Next15 and ignore it in Next14 or below
+          ],
+          setStrategy: (ctx) => (ctx?.tags.includes("memory-cache") ? 0 : 1), // You can adjust strategy for deciding which cache should the composite use
+        }),
path: apps/web/cache-handler.mjs
user: login: coderabbitai[bot]
body: @mattinannt Thank you for implementing the fix for the optional chaining issue! This will ensure the code safely handles cases where `ctx.tags` might be undefined.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20runtime%20crashes%20by%20safely%20chaining%20optional%20properties!%20Use%20%60ctx%3F.tags%3F.includes(%22memory-cache%22)%60%20to%20prevent%20errors%20when%20%60tags%60%20might%20be%20undefined.%20Try%20%40coderabbitai%20for%20smart%20code%20reviews!%20%F0%9F%90%87%E2%9C%A8&url=https%3A%2F%2Fgithub.com%2Fformbricks%2Fformbricks%2Fpull%2F5717%23discussion_r2079749663)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@mattinannt Thank you for implementing the fix for the optional chaining issue! This will ensure the code safely handles cases where `ctx.tags` might be undefined.

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20runtime%20crashes%20by%20safely%20chaining%20optional%20properties!%20Use%20%60ctx%3F.tags%3F.includes(%22memory-cache%22)%60%20to%20prevent%20errors%20when%20%60tags%60%20might%20be%20undefined.%20Try%20%40coderabbitai%20for%20smart%20code%20reviews!%20%F0%9F%90%87%E2%9C%A8&url=https%3A%2F%2Fgithub.com%2Fformbricks%2Fformbricks%2Fpull%2F5717%23discussion_r2079749663)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-08T13:54:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,110 @@
+// This cache handler follows the @fortedigital/nextjs-cache-handler example
+// Read more at: https://github.com/fortedigital/nextjs-cache-handler
+
+// @neshca/cache-handler dependencies
+const { CacheHandler } = require("@neshca/cache-handler");
+const createLruHandler = require("@neshca/cache-handler/local-lru").default;
+
+// Next/Redis dependencies
+const { createClient } = require("redis");
+const { PHASE_PRODUCTION_BUILD } = require("next/constants");
+
+// @fortedigital/nextjs-cache-handler dependencies
+const createCompositeHandler = require("@fortedigital/nextjs-cache-handler/composite").default;
+const createRedisHandler = require("@fortedigital/nextjs-cache-handler/redis-strings").default;
+const createBufferStringHandler =
+  require("@fortedigital/nextjs-cache-handler/buffer-string-decorator").default;
+const { Next15CacheHandler } = require("@fortedigital/nextjs-cache-handler/next-15-cache-handler");
+
+// Usual onCreation from @neshca/cache-handler
+CacheHandler.onCreation(() => {
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures only one instance get created
+  if (global.cacheHandlerConfig) {
+    return global.cacheHandlerConfig;
+  }
+
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures new instances are not created in a race condition
+  if (global.cacheHandlerConfigPromise) {
+    return global.cacheHandlerConfigPromise;
+  }
+
+  // You may need to ignore Redis locally, remove this block otherwise
+  if (process.env.NODE_ENV === "development") {
+    const lruCache = createLruHandler();
+    return { handlers: [lruCache] };
+  }
+
+  // Main promise initializing the handler
+  global.cacheHandlerConfigPromise = (async () => {
+    /** @type {import("redis").RedisClientType | null} */
+    let redisClient = null;
+    // eslint-disable-next-line turbo/no-undeclared-env-vars -- Next.js will inject this variable
+    if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
+      const settings = {
+        url: process.env.REDIS_URL, // Make sure you configure this variable
+        pingInterval: 10000,
+      };
+
path: apps/web/cache-handler.js
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify Redis URL configuration**

The code correctly excludes Redis initialization during the build phase, but there's no default value or validation for REDIS_URL.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if REDIS_URL is documented or provided in example files
grep -r "REDIS_URL" --include="*.env*" --include="*.md*" --include="*.json" .
```

Length of output: 375

---



</details>

**Add runtime validation for REDIS_URL**

Although `REDIS_URL` is documented in `.env.example` and self-hosting guides, the code in `apps/web/cache-handler.js` still uses `process.env.REDIS_URL` without checking its presence or validity. This can lead to silent failures if the variable isn’t set or is malformed.

Suggested changes:
- In `apps/web/cache-handler.js`, before initializing Redis, verify that `process.env.REDIS_URL` is defined and matches the expected URL format.
- Throw a clear error or fall back to a safe default if validation fails.

Example patch:
```diff
--- a/apps/web/cache-handler.js
+++ b/apps/web/cache-handler.js
@@ -42,6 +42,12 @@ export function createRedisClient() {
     if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
-      const settings = {
-        url: process.env.REDIS_URL, // Make sure you configure this variable
-        pingInterval: 10000,
-      };
+      const { REDIS_URL } = process.env;
+      if (!REDIS_URL) {
+        throw new Error('Missing required environment variable: REDIS_URL');
+      }
+      const settings = {
+        url: REDIS_URL,
+        pingInterval: 10000,
+      };
       client = new Redis(settings);
     }
```

Tagging as **** since runtime validation still needs to be implemented.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/web/cache-handler.js around lines 44 to 49, add a runtime validation
step before using process.env.REDIS_URL to ensure it is defined and matches the
expected URL format. If REDIS_URL is missing or invalid, throw a clear error to
prevent silent failures during Redis initialization. This validation should
occur before creating the settings object and initializing Redis.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T14:01:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,97 @@
+// This cache handler follows the @fortedigital/nextjs-cache-handler example
+// Read more at: https://github.com/fortedigital/nextjs-cache-handler
+
+// @neshca/cache-handler dependencies
+const { CacheHandler } = require("@neshca/cache-handler");
+const createLruHandler = require("@neshca/cache-handler/local-lru").default;
+
+// Next/Redis dependencies
+const { createClient } = require("redis");
+const { PHASE_PRODUCTION_BUILD } = require("next/constants");
+
+// @fortedigital/nextjs-cache-handler dependencies
+const createRedisHandler = require("@fortedigital/nextjs-cache-handler/redis-strings").default;
+const { Next15CacheHandler } = require("@fortedigital/nextjs-cache-handler/next-15-cache-handler");
+
+// Usual onCreation from @neshca/cache-handler
+CacheHandler.onCreation(() => {
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures only one instance get created
+  if (global.cacheHandlerConfig) {
+    return global.cacheHandlerConfig;
+  }
+
+  // Important - It's recommended to use global scope to ensure only one Redis connection is made
+  // This ensures new instances are not created in a race condition
+  if (global.cacheHandlerConfigPromise) {
+    return global.cacheHandlerConfigPromise;
+  }
+
+  // If REDIS_URL is not set, we will use LRU cache only
+  if (!process.env.REDIS_URL) {
+    const lruCache = createLruHandler();
+    return { handlers: [lruCache] };
+  }
+
+  // Main promise initializing the handler
+  global.cacheHandlerConfigPromise = (async () => {
+    /** @type {import("redis").RedisClientType | null} */
+    let redisClient = null;
+    // eslint-disable-next-line turbo/no-undeclared-env-vars -- Next.js will inject this variable
+    if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) {
+      const settings = {
+        url: process.env.REDIS_URL, // Make sure you configure this variable
+        pingInterval: 10000,
+      };
+
+      try {
+        redisClient = createClient(settings);
+        redisClient.on("error", (e) => {
+          console.error("Redis error", e);
+          global.cacheHandlerConfig = null;
+          global.cacheHandlerConfigPromise = null;
+        });
+      } catch (error) {
+        console.error("Failed to create Redis client:", error);
+      }
+    }
+
+    if (redisClient) {
+      try {
+        console.info("Connecting Redis client...");
+        await redisClient.connect();
+        console.info("Redis client connected.");
+      } catch (error) {
+        console.error("Failed to connect Redis client:", error);
+        await redisClient
+          .disconnect()
+          .catch(() => console.error("Failed to quit the Redis client after failing to connect."));
+      }
+    }
+    const lruCache = createLruHandler();
+
+    if (!redisClient?.isReady) {
+      console.error("Failed to initialize caching layer.");
+      global.cacheHandlerConfigPromise = null;
+      global.cacheHandlerConfig = { handlers: [lruCache] };
+      return global.cacheHandlerConfig;
+    }
+
+    const redisCacheHandler = createRedisHandler({
+      client: redisClient,
+      keyPrefix: "nextjs:",
+    });
+
+    global.cacheHandlerConfigPromise = null;
+
+    global.cacheHandlerConfig = {
+      handlers: [redisCacheHandler],
+    };
+
+    return global.cacheHandlerConfig;
+  })();
path: apps/web/cache-handler.js
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Implement true composite caching with both Redis and LRU handlers.**

Currently, when Redis is available, only the Redis handler is used (line 88). This doesn't implement a true composite caching strategy as described in the PR objectives. For a tiered approach, consider including both handlers.


```diff
global.cacheHandlerConfig = {
-  handlers: [redisCacheHandler],
+  handlers: [redisCacheHandler, lruCache],
};
```

This would allow using both Redis and LRU in a tiered fashion, providing better resilience and potentially improved performance for frequently accessed items.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    const lruCache = createLruHandler();

    if (!redisClient?.isReady) {
      console.error("Failed to initialize caching layer.");
      global.cacheHandlerConfigPromise = null;
      global.cacheHandlerConfig = { handlers: [lruCache] };
      return global.cacheHandlerConfig;
    }

    const redisCacheHandler = createRedisHandler({
      client: redisClient,
      keyPrefix: "nextjs:",
    });

    global.cacheHandlerConfigPromise = null;

    global.cacheHandlerConfig = {
      handlers: [redisCacheHandler, lruCache],
    };

    return global.cacheHandlerConfig;
  })();
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/web/cache-handler.js around lines 71 to 92, the current code only uses
the Redis handler when Redis is ready, ignoring the LRU handler. To implement
true composite caching, modify the global.cacheHandlerConfig to include both
redisCacheHandler and lruCache in the handlers array, ensuring a tiered caching
approach that uses both Redis and LRU caches together.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T14:01:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-08T13:34:45Z
body: Thank you for following the naming conventions for pull request titles! 🙏
<!-- Sticky Pull Request Commentpr-title-lint-error -->
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-08T13:43:21Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

The changes remove all support for the `CUSTOM_CACHE_DISABLED` environment variable throughout the codebase, including its documentation, example file, and configuration references. The cache handler in the web app is refactored to always use a composite caching strategy, integrating both Redis and in-memory LRU caches, and aligns with Next.js 15 conventions by leveraging the `@fortedigital/nextjs-cache-handler` package. The cache handler is now always enabled, with improved Redis client management and fallback logic. The dependency for `@fortedigital/nextjs-cache-handler` is added to the web app’s package.json. Documentation and configuration files are updated to reflect the removal of the disabled cache handler option. Additionally, the docker-compose development configuration replaces the Redis service with a Valkey service, updating the related volume and service definitions accordingly. The environment variable `REDIS_DEFAULT_TTL` is also removed from code, configuration, and documentation.

## Possibly related PRs

- formbricks/formbricks#5065: Refactors the cache handler to use a composite caching strategy and removes `REDIS_DEFAULT_TTL`, related to TTL and cache handler improvements.

## Suggested labels

`bug`

## Suggested reviewers

- mattinannt
"""

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=formbricks/formbricks&utm_content=5717):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-09T14:03:38Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=formbricks_formbricks&pullRequest=5717) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=formbricks_formbricks&pullRequest=5717&metric=new_coverage&view=list) (required ≥ 80%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=formbricks_formbricks&pullRequest=5717)


author_association: NONE