mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 18:30:32 -06:00
fix: nextjs cache handler for next 15 (#5717)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
@@ -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=
|
||||
|
||||
|
||||
97
apps/web/cache-handler.js
Normal file
97
apps/web/cache-handler.js
Normal file
@@ -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();
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user