feat: make cubejs mandatory for xm suite v5

This commit is contained in:
Bhagya Amarasinghe
2026-04-29 16:08:24 +05:30
parent fbe2a31133
commit 0df16f6f0c
23 changed files with 604 additions and 159 deletions
+5 -5
View File
@@ -28,12 +28,12 @@ The script will prompt you for the following information:
That's it! After running the command and providing the required information, visit the domain name you entered, and you should see the Formbricks home wizard!
## Formbricks Hub
## Formbricks Hub and Cube
The stack includes the [Formbricks Hub](https://github.com/formbricks/hub) API (`ghcr.io/formbricks/hub`). Hub shares the same database as Formbricks by default.
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.
- **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` (required). `HUB_API_URL` defaults to `http://hub:8080` so the Formbricks app can reach Hub inside the compose network. Override `HUB_DATABASE_URL` only if you want Hub to use a separate database.
- **Development** (`docker-compose.dev.yml`): Hub uses the same Postgres database; `HUB_API_KEY` defaults to `dev-api-key` (override with `HUB_API_KEY`) and the local Hub URL is `http://localhost:8080`.
- **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. 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`.
In development, Hub is exposed locally on port **8080**. In production Docker Compose, Hub stays internal to the compose network and is reached via `http://hub:8080`.
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`.
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
// queryRewrite runs before every Cube query. Use it to enforce row-level security (RLS)
// by injecting filters based on the caller's identity (e.g. organizationId, projectId).
//
// The securityContext is populated from the decoded JWT passed via the API token.
// Currently a passthrough because access control is handled in the Next.js API layer
// before reaching Cube. When Cube is exposed more broadly or multi-tenancy enforcement
// is needed at the Cube level, add filters here based on securityContext claims.
queryRewrite: (query, { securityContext }) => {
return query;
},
};
+165
View File
@@ -0,0 +1,165 @@
// This schema maps to the `feedback_records` table owned by the Formbricks Hub Postgres.
// If the Hub changes column names, types, or the metadata JSONB shape (e.g. the `topics` array),
// this schema must be updated to match.
cube(`FeedbackRecords`, {
sql: `SELECT * FROM feedback_records`,
measures: {
count: {
type: `count`,
description: `Total number of feedback responses`,
},
promoterCount: {
type: `count`,
filters: [{ sql: `${CUBE}.value_number >= 9` }],
description: `Number of promoters (NPS score 9-10)`,
},
detractorCount: {
type: `count`,
filters: [{ sql: `${CUBE}.value_number <= 6` }],
description: `Number of detractors (NPS score 0-6)`,
},
passiveCount: {
type: `count`,
filters: [{ sql: `${CUBE}.value_number >= 7 AND ${CUBE}.value_number <= 8` }],
description: `Number of passives (NPS score 7-8)`,
},
npsScore: {
type: `number`,
sql: `
CASE
WHEN COUNT(*) = 0 THEN 0
ELSE ROUND(
(
(COUNT(CASE WHEN ${CUBE}.value_number >= 9 THEN 1 END)::numeric -
COUNT(CASE WHEN ${CUBE}.value_number <= 6 THEN 1 END)::numeric)
/ COUNT(*)::numeric
) * 100,
2
)
END
`,
description: `Net Promoter Score: ((Promoters - Detractors) / Total) * 100`,
},
averageScore: {
type: `avg`,
sql: `${CUBE}.value_number`,
description: `Average NPS score`,
},
},
dimensions: {
id: {
sql: `id`,
type: `string`,
primaryKey: true,
},
sentiment: {
sql: `sentiment`,
type: `string`,
description: `Sentiment extracted from metadata JSONB field`,
},
sourceType: {
sql: `source_type`,
type: `string`,
description: `Source type of the feedback (e.g., nps_campaign, survey)`,
},
sourceName: {
sql: `source_name`,
type: `string`,
description: `Human-readable name of the source`,
},
fieldType: {
sql: `field_type`,
type: `string`,
description: `Type of feedback field (e.g., nps, text, rating)`,
},
collectedAt: {
sql: `collected_at`,
type: `time`,
description: `Timestamp when the feedback was collected`,
},
npsValue: {
sql: `value_number`,
type: `number`,
description: `Raw NPS score value (0-10)`,
},
responseId: {
sql: `response_id`,
type: `string`,
description: `Unique identifier linking related feedback records`,
},
userIdentifier: {
sql: `user_identifier`,
type: `string`,
description: `Identifier of the user who provided feedback`,
},
emotion: {
sql: `emotion`,
type: `string`,
description: `Emotion extracted from metadata JSONB field`,
},
tenantId: {
sql: `tenant_id`,
type: `string`,
description: `Tenant ID linking to FeedbackRecordDirectory`,
},
},
joins: {
TopicsUnnested: {
sql: `${CUBE}.id = ${TopicsUnnested}.feedback_record_id`,
relationship: `hasMany`,
},
},
});
cube(`TopicsUnnested`, {
sql: `
SELECT
fr.id as feedback_record_id,
topic_elem.topic
FROM feedback_records fr
CROSS JOIN LATERAL jsonb_array_elements_text(COALESCE(fr.metadata->'topics', '[]'::jsonb)) AS topic_elem(topic)
`,
measures: {
count: {
type: `count`,
},
},
dimensions: {
id: {
sql: `feedback_record_id || '-' || topic`,
type: `string`,
primaryKey: true,
},
feedbackRecordId: {
sql: `feedback_record_id`,
type: `string`,
},
topic: {
sql: `topic`,
type: `string`,
description: `Individual topic from the topics array`,
},
},
});
+26
View File
@@ -38,6 +38,10 @@ 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.
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}
# Set the minimum log level(debug, info, warn, error, fatal)
# LOG_LEVEL: info
@@ -285,6 +289,28 @@ 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.
cube:
restart: always
image: cubejs/cube:v1.6.6
depends_on:
hub-migrate:
condition: service_completed_successfully
postgres:
condition: service_healthy
environment:
CUBEJS_DB_TYPE: postgres
CUBEJS_DB_HOST: ${CUBEJS_DB_HOST:-postgres}
CUBEJS_DB_NAME: ${CUBEJS_DB_NAME:-formbricks}
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_CACHE_AND_QUEUE_DRIVER: memory
volumes:
- ./cube/cube.js:/cube/conf/cube.js:ro
- ./cube/schema:/cube/conf/model:ro
volumes:
postgres:
driver: local
+13 -1
View File
@@ -313,6 +313,10 @@ EOT
echo "📥 Downloading docker-compose.yml from Formbricks GitHub repository..."
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/docker-compose.yml
mkdir -p cube/schema
echo "📥 Downloading Cube.js configuration for XM Suite v5 analytics..."
curl -fsSL -o cube/cube.js https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/cube/cube.js
curl -fsSL -o cube/schema/FeedbackRecords.js https://raw.githubusercontent.com/formbricks/formbricks/stable/docker/cube/schema/FeedbackRecords.js
echo "🚙 Updating docker-compose.yml with your custom inputs..."
sed -i "/WEBAPP_URL:/s|WEBAPP_URL:.*|WEBAPP_URL: \"https://$domain_name\"|" docker-compose.yml
@@ -326,6 +330,14 @@ EOT
cron_secret=$(openssl rand -hex 32) && sed -i "/CRON_SECRET:$/s/CRON_SECRET:.*/CRON_SECRET: $cron_secret/" docker-compose.yml
echo "🚗 CRON_SECRET updated successfully!"
hub_api_key=$(openssl rand -hex 32)
cubejs_api_secret=$(openssl rand -hex 32)
cat <<EOF > .env
HUB_API_KEY=$hub_api_key
CUBEJS_API_SECRET=$cubejs_api_secret
EOF
echo "🚗 Generated Hub and Cube secrets in .env successfully!"
if [[ -n $mail_from ]]; then
sed -i "s|# MAIL_FROM:|MAIL_FROM: \"$mail_from\"|" docker-compose.yml
@@ -796,4 +808,4 @@ uninstall)
echo "🚀 Executing default step of installing Formbricks"
install_formbricks
;;
esac
esac