mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-21 03:31:20 -05:00
fix: make bundled cube optional
This commit is contained in:
+3
-1
@@ -68,7 +68,9 @@ HUB_DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres?sslmode=d
|
||||
###########################
|
||||
# CUBE ANALYTICS (XM V5) #
|
||||
###########################
|
||||
# XM Suite v5 analysis features require Cube.js. The local dev stack exposes Cube on port 4000.
|
||||
# XM Suite v5 analysis features require Cube.js. The optional xm dev profile exposes Cube on port 4000.
|
||||
# Uncomment COMPOSE_PROFILES=xm to run the optional Cube analytics service.
|
||||
# COMPOSE_PROFILES=xm
|
||||
CUBEJS_API_URL=http://localhost:4000
|
||||
# Generate with: openssl rand -hex 32. `pnpm dev:setup` will create/preserve this automatically.
|
||||
CUBEJS_API_SECRET=
|
||||
|
||||
@@ -109,6 +109,16 @@ describe("env", () => {
|
||||
expect(env.CUBEJS_API_SECRET).toBeUndefined();
|
||||
});
|
||||
|
||||
test("treats an empty Cube API secret from Docker Compose as omitted", async () => {
|
||||
setTestEnv({
|
||||
CUBEJS_API_SECRET: "",
|
||||
});
|
||||
|
||||
const { env } = await import("./env");
|
||||
|
||||
expect(env.CUBEJS_API_SECRET).toBeUndefined();
|
||||
});
|
||||
|
||||
test("allows the Cube API URL to be omitted until analytics is used", async () => {
|
||||
setTestEnv({
|
||||
CUBEJS_API_URL: undefined,
|
||||
@@ -119,6 +129,16 @@ describe("env", () => {
|
||||
expect(env.CUBEJS_API_URL).toBeUndefined();
|
||||
});
|
||||
|
||||
test("treats an empty Cube API URL as omitted", async () => {
|
||||
setTestEnv({
|
||||
CUBEJS_API_URL: "",
|
||||
});
|
||||
|
||||
const { env } = await import("./env");
|
||||
|
||||
expect(env.CUBEJS_API_URL).toBeUndefined();
|
||||
});
|
||||
|
||||
test("fails to load when the Cube API URL is invalid", async () => {
|
||||
setTestEnv({
|
||||
CUBEJS_API_URL: "not-a-url",
|
||||
|
||||
+8
-4
@@ -141,6 +141,10 @@ const ZSurveySchedulingTimeZone = z.string().trim().min(1).refine(isValidIanaTim
|
||||
|
||||
const ZSurveySchedulingLocalHour = z.coerce.number().int().min(0).max(23);
|
||||
const ZSurveySchedulingLocalMinute = z.coerce.number().int().min(0).max(59);
|
||||
const emptyStringToUndefined = (value: unknown) =>
|
||||
typeof value === "string" && value.trim() === "" ? undefined : value;
|
||||
const ZOptionalNonEmptyString = z.preprocess(emptyStringToUndefined, z.string().trim().min(1).optional());
|
||||
const ZOptionalUrl = z.preprocess(emptyStringToUndefined, z.url().optional());
|
||||
|
||||
const parsedEnv = createEnv({
|
||||
/*
|
||||
@@ -194,10 +198,10 @@ const parsedEnv = createEnv({
|
||||
AI_AZURE_API_KEY: z.string().optional(),
|
||||
AI_AZURE_API_VERSION: z.string().optional(),
|
||||
AI_AZURE_RESOURCE_NAME: z.string().optional(),
|
||||
CUBEJS_API_SECRET: z.string().trim().min(1).optional(),
|
||||
CUBEJS_API_URL: z.url().optional(),
|
||||
CUBEJS_JWT_AUDIENCE: z.string().trim().min(1).optional(),
|
||||
CUBEJS_JWT_ISSUER: z.string().trim().min(1).optional(),
|
||||
CUBEJS_API_SECRET: ZOptionalNonEmptyString,
|
||||
CUBEJS_API_URL: ZOptionalUrl,
|
||||
CUBEJS_JWT_AUDIENCE: ZOptionalNonEmptyString,
|
||||
CUBEJS_JWT_ISSUER: ZOptionalNonEmptyString,
|
||||
HTTP_PROXY: z.url().optional(),
|
||||
HTTPS_PROXY: z.url().optional(),
|
||||
HUB_API_URL: z.url(),
|
||||
|
||||
@@ -2,7 +2,10 @@ import { createRequire } from "node:module";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { queryRewrite } = require("../../../../../../../docker/cube/cube.js") as {
|
||||
const cubeConfigPath = require.resolve("../../../../../../../docker/cube/cube.js");
|
||||
process.env.CUBEJS_API_SECRET = process.env.CUBEJS_API_SECRET || "cube-secret";
|
||||
|
||||
const { queryRewrite } = require(cubeConfigPath) as {
|
||||
queryRewrite: (
|
||||
query: Record<string, unknown>,
|
||||
context: { securityContext?: Record<string, unknown> }
|
||||
@@ -34,6 +37,17 @@ describe("cube queryRewrite", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects Cube startup without an API secret", () => {
|
||||
const originalSecret = process.env.CUBEJS_API_SECRET;
|
||||
delete process.env.CUBEJS_API_SECRET;
|
||||
delete require.cache[cubeConfigPath];
|
||||
|
||||
expect(() => require(cubeConfigPath)).toThrow(/CUBEJS_API_SECRET is required to run Cube/);
|
||||
|
||||
process.env.CUBEJS_API_SECRET = originalSecret || "cube-secret";
|
||||
delete require.cache[cubeConfigPath];
|
||||
});
|
||||
|
||||
test("rejects queries without a rewrite context", () => {
|
||||
expect(() =>
|
||||
queryRewrite(
|
||||
|
||||
@@ -81,6 +81,7 @@ services:
|
||||
PGSSLMODE: disable
|
||||
|
||||
cube:
|
||||
profiles: ["xm"]
|
||||
image: cubejs/cube:v1.6.6
|
||||
env_file:
|
||||
- apps/web/.env
|
||||
@@ -100,7 +101,7 @@ services:
|
||||
CUBEJS_DB_PASS: ${CUBEJS_DB_PASS:-postgres}
|
||||
CUBEJS_DB_PORT: ${CUBEJS_DB_PORT:-5432}
|
||||
CUBEJS_DEV_MODE: "true"
|
||||
CUBEJS_API_SECRET: ${CUBEJS_API_SECRET:?CUBEJS_API_SECRET is required to run Cube}
|
||||
CUBEJS_API_SECRET: ${CUBEJS_API_SECRET:-}
|
||||
CUBEJS_JWT_ISSUER: ${CUBEJS_JWT_ISSUER:-formbricks-web}
|
||||
CUBEJS_JWT_AUDIENCE: ${CUBEJS_JWT_AUDIENCE:-formbricks-cube}
|
||||
CUBEJS_DEFAULT_API_SCOPES: meta,data
|
||||
|
||||
+4
-4
@@ -30,10 +30,10 @@ That's it! After running the command and providing the required information, vis
|
||||
|
||||
## Formbricks Hub and Cube
|
||||
|
||||
The stack includes the [Formbricks Hub](https://github.com/formbricks/hub) API (`ghcr.io/formbricks/hub`) and a bundled Cube.js service for XM Suite v5 analytics. Hub and Cube share the same database as Formbricks by default.
|
||||
The stack includes the [Formbricks Hub](https://github.com/formbricks/hub) API (`ghcr.io/formbricks/hub`) and can also run a bundled Cube.js service for XM Suite v5 analytics. Hub and Cube share the same database as Formbricks by default, and Cube is enabled through the optional Docker Compose `xm` profile.
|
||||
|
||||
- **Migrations**: A `hub-migrate` service runs Hub's database migrations (goose + river) before the Hub API starts. It runs on every `docker compose up` and is idempotent.
|
||||
- **Production** (`docker/docker-compose.yml`): Set `HUB_API_KEY` and `CUBEJS_API_SECRET` (required). `HUB_API_URL` defaults to `http://hub:8080` and `CUBEJS_API_URL` defaults to `http://cube:4000` so the Formbricks app can reach both services inside the compose network. Cube JWT issuer/audience default to `formbricks-web` and `formbricks-cube`, and the bundled Cube service exposes only `meta,data` API scopes. Override `HUB_DATABASE_URL` and `CUBEJS_DB_*` only if Hub or Cube should use a separate database.
|
||||
- **Development** (`docker-compose.dev.yml`): Hub and Cube use the same local Postgres database. `HUB_API_KEY` defaults to `dev-api-key`, `CUBEJS_API_URL` defaults to `http://localhost:4000`, and `pnpm dev:setup` generates `CUBEJS_API_SECRET` in the repo root `.env`.
|
||||
- **Production** (`docker/docker-compose.yml`): Set `HUB_API_KEY` (required). `HUB_API_URL` defaults to `http://hub:8080` so the Formbricks app can reach Hub inside the compose network. To enable XM Suite v5 analytics, set `COMPOSE_PROFILES=xm` and `CUBEJS_API_SECRET`; `CUBEJS_API_URL` defaults to `http://cube:4000`. Cube JWT issuer/audience default to `formbricks-web` and `formbricks-cube`, and the bundled Cube service exposes only `meta,data` API scopes. Override `HUB_DATABASE_URL` and `CUBEJS_DB_*` only if Hub or Cube should use a separate database.
|
||||
- **Development** (`docker-compose.dev.yml`): Hub uses the same local Postgres database and `HUB_API_KEY` defaults to `dev-api-key`. Cube is behind the `xm` profile, `CUBEJS_API_URL` defaults to `http://localhost:4000`, and `pnpm dev:setup` generates `CUBEJS_API_SECRET` in the repo root `.env`.
|
||||
|
||||
In development, Hub is exposed locally on port **8080** and Cube on **4000** (with the Cube playground on **4001**). In production Docker Compose, Hub and Cube stay internal to the compose network and are reached via `http://hub:8080` and `http://cube:4000`.
|
||||
In development, Hub is exposed locally on port **8080**. When the `xm` profile is enabled, Cube is exposed on **4000** (with the Cube playground on **4001**). In production Docker Compose, Hub stays internal to the compose network at `http://hub:8080`; Cube also stays internal at `http://cube:4000` when enabled.
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
const TENANT_MEMBER = "FeedbackRecords.tenantId";
|
||||
const REQUIRED_SCOPE = "xm:cube:query";
|
||||
|
||||
function assertRequiredEnvironmentVariable(name) {
|
||||
const value = process.env[name];
|
||||
|
||||
if (typeof value !== "string" || value.trim().length === 0) {
|
||||
throw new Error(`${name} is required to run Cube`);
|
||||
}
|
||||
}
|
||||
|
||||
assertRequiredEnvironmentVariable("CUBEJS_API_SECRET");
|
||||
|
||||
function getStringClaim(securityContext, claim) {
|
||||
const value = securityContext?.[claim];
|
||||
if (typeof value !== "string") {
|
||||
|
||||
@@ -38,9 +38,9 @@ x-environment: &environment
|
||||
# Hub database URL (optional). Default: same Postgres as Formbricks. Set only if Hub uses a separate DB.
|
||||
# HUB_DATABASE_URL:
|
||||
|
||||
# Cube.js analytics for XM Suite v5. Cube runs inside this compose stack by default.
|
||||
# Cube.js analytics for XM Suite v5. Enable the optional xm profile and set CUBEJS_API_SECRET to run Cube.
|
||||
CUBEJS_API_URL: ${CUBEJS_API_URL:-http://cube:4000}
|
||||
CUBEJS_API_SECRET: ${CUBEJS_API_SECRET:?CUBEJS_API_SECRET is required to run XM Suite v5 analytics}
|
||||
CUBEJS_API_SECRET: ${CUBEJS_API_SECRET:-}
|
||||
CUBEJS_JWT_ISSUER: ${CUBEJS_JWT_ISSUER:-formbricks-web}
|
||||
CUBEJS_JWT_AUDIENCE: ${CUBEJS_JWT_AUDIENCE:-formbricks-cube}
|
||||
|
||||
@@ -291,8 +291,9 @@ services:
|
||||
API_KEY: ${HUB_API_KEY:?HUB_API_KEY is required to run Hub}
|
||||
DATABASE_URL: ${HUB_DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/formbricks?sslmode=disable}
|
||||
|
||||
# Cube.js analytics service for XM Suite v5. Shares the Hub database by default.
|
||||
# Optional Cube.js analytics service for XM Suite v5. Enable with COMPOSE_PROFILES=xm and set CUBEJS_API_SECRET.
|
||||
cube:
|
||||
profiles: ["xm"]
|
||||
restart: always
|
||||
image: cubejs/cube:v1.6.6
|
||||
depends_on:
|
||||
@@ -307,7 +308,7 @@ services:
|
||||
CUBEJS_DB_USER: ${CUBEJS_DB_USER:-postgres}
|
||||
CUBEJS_DB_PASS: ${CUBEJS_DB_PASS:-postgres}
|
||||
CUBEJS_DB_PORT: ${CUBEJS_DB_PORT:-5432}
|
||||
CUBEJS_API_SECRET: ${CUBEJS_API_SECRET:?CUBEJS_API_SECRET is required to run Cube}
|
||||
CUBEJS_API_SECRET: ${CUBEJS_API_SECRET:-}
|
||||
CUBEJS_JWT_ISSUER: ${CUBEJS_JWT_ISSUER:-formbricks-web}
|
||||
CUBEJS_JWT_AUDIENCE: ${CUBEJS_JWT_AUDIENCE:-formbricks-cube}
|
||||
CUBEJS_DEFAULT_API_SCOPES: meta,data
|
||||
|
||||
@@ -334,6 +334,7 @@ EOT
|
||||
hub_api_key=$(openssl rand -hex 32)
|
||||
cubejs_api_secret=$(openssl rand -hex 32)
|
||||
cat <<EOF > .env
|
||||
COMPOSE_PROFILES=xm
|
||||
HUB_API_KEY=$hub_api_key
|
||||
CUBEJS_API_SECRET=$cubejs_api_secret
|
||||
CUBEJS_JWT_ISSUER=formbricks-web
|
||||
|
||||
@@ -37,7 +37,7 @@ The controls assume query bodies are attacker-influenced. Tenant identity is nev
|
||||
</Step>
|
||||
<Step title="Enforce the tenant filter">
|
||||
Cube `queryRewrite` rejects missing tenant context, rejects caller-supplied tenant member usage, and
|
||||
appends `FeedbackRecords.tenantId = securityContext.tenantId` to every query.
|
||||
appends `FeedbackRecords.tenantId = securityContext.workspaceId` to every query.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ For `AI_PROVIDER=google`, use a Gemini model ID such as `gemini-2.5-flash` toget
|
||||
#### Formbricks Hub
|
||||
|
||||
When running the stack with [Formbricks Hub](https://github.com/formbricks/hub) (for example via Docker Compose or Helm), the following variables apply:
|
||||
The bundled Docker Compose stack starts Hub by default.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| ---------------- | ---------------------------------------------------------------------------------- | -------- | --------------------------------------------------- |
|
||||
@@ -110,6 +111,7 @@ When running the stack with [Formbricks Hub](https://github.com/formbricks/hub)
|
||||
|
||||
XM Suite v5 dashboard and analysis features require a reachable Cube.js instance. Formbricks generates the backend
|
||||
Cube JWT from `CUBEJS_API_SECRET`, so `CUBEJS_API_TOKEN` is not part of the supported setup contract.
|
||||
If you do not use XM Suite v5 analytics, omit the Cube variables and leave the bundled Docker `xm` profile disabled.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------- | ------------------------------------ |
|
||||
|
||||
@@ -36,12 +36,15 @@ Make sure Docker and Docker Compose are installed on your system. These are usua
|
||||
curl -o cube/schema/FeedbackRecords.js https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/cube/schema/FeedbackRecords.js
|
||||
```
|
||||
|
||||
1. **Generate Hub and Cube Secrets**
|
||||
1. **Generate Hub Secret and Optional Cube Secret**
|
||||
|
||||
XM Suite v5 analytics requires Formbricks Hub and Cube.js. Create a local `.env` file for the required shared secrets:
|
||||
Formbricks Hub requires an API key. XM Suite v5 analytics also requires Cube.js; set the optional `xm`
|
||||
Compose profile and Cube secret when you want to run the bundled Cube service. For a Hub-only stack, create
|
||||
`.env` with just `HUB_API_KEY` and omit `COMPOSE_PROFILES` and `CUBEJS_API_SECRET`.
|
||||
|
||||
```bash
|
||||
cat <<EOF > .env
|
||||
COMPOSE_PROFILES=xm
|
||||
HUB_API_KEY=$(openssl rand -hex 32)
|
||||
CUBEJS_API_SECRET=$(openssl rand -hex 32)
|
||||
CUBEJS_JWT_ISSUER=formbricks-web
|
||||
@@ -100,7 +103,9 @@ Make sure Docker and Docker Compose are installed on your system. These are usua
|
||||
|
||||
1. **Start the Docker Setup**
|
||||
|
||||
Now, you're ready to run Formbricks with Docker. Use the command below to start Formbricks together with PostgreSQL, Redis, Formbricks Hub, and Cube.js:
|
||||
Now, you're ready to run Formbricks with Docker. Use the command below to start Formbricks together with
|
||||
PostgreSQL, Redis, and Formbricks Hub. If the `xm` profile is set in `.env`, Docker Compose also starts Cube.js
|
||||
for XM Suite v5 analytics.
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
@@ -113,8 +118,8 @@ Make sure Docker and Docker Compose are installed on your system. These are usua
|
||||
Once the setup is running, open [**http://localhost:3000**](http://localhost:3000) in your browser to access Formbricks. The first time you visit, you'll see a setup wizard. Follow the steps to create your first user and start using Formbricks.
|
||||
|
||||
<Note>
|
||||
The bundled Docker stack keeps Formbricks Hub and Cube.js internal to the compose network. The app reaches
|
||||
them through `http://hub:8080` and `http://cube:4000`.
|
||||
The bundled Docker stack keeps Formbricks Hub internal to the compose network. When the `xm` profile is
|
||||
enabled, Cube.js is internal too. The app reaches them through `http://hub:8080` and `http://cube:4000`.
|
||||
</Note>
|
||||
|
||||
## Update
|
||||
|
||||
Reference in New Issue
Block a user