Formbricks Self-Hosting
CADENSA uses Formbricks for in-app NPS surveys and the feedback button. Instead of the Formbricks cloud, we run a self-hosted instance in our EU infrastructure — no US data transfer, full GDPR compliance.
Self-hosted URL: https://surveys.cadensa.io (Hetzner, Germany)
Why self-host?
| Formbricks Cloud | Self-hosted (surveys.cadensa.io) | |
|---|---|---|
| Data location | Formbricks servers (US/EU mixed) | Hetzner Germany (EU only) |
| US data transfer | Possible | None |
| GDPR | Requires DPA | No third-party DPA needed |
| Consent gate | Required | Required (analytics category) |
Architecture
cadensa-frontend / cadensa-landing
└─ @formbricks/js SDK
└─ VITE_FORMBRICKS_APP_URL / NEXT_PUBLIC_FORMBRICKS_APP_URL
└─ https://surveys.cadensa.io
└─ Formbricks app (Kubernetes: formbricks namespace)
└─ PostgreSQL 15 (StatefulSet: formbricks-postgres)
The Formbricks app runs in its own Kubernetes namespace (formbricks) with a dedicated PostgreSQL StatefulSet. It is exposed via nginx Ingress with a Let's Encrypt TLS certificate.
Kubernetes deployment
All manifests live in cadensa-k8s/k8s/formbricks/:
| File | Description |
|---|---|
namespace.yaml | formbricks namespace |
postgres-statefulset.yaml | PostgreSQL 15, 10 Gi PVC |
formbricks-configmap.yaml | Non-sensitive config (URL, SMTP host, etc.) |
formbricks-sealed-secret.yaml | Sealed Secret template (replace with real values) |
formbricks-deployment.yaml | Formbricks app + Service |
formbricks-ingress.yaml | nginx Ingress with TLS (surveys.cadensa.io) |
First-time deploy
# 1. Generate secrets
NEXTAUTH_SECRET=$(openssl rand -base64 32)
ENCRYPTION_KEY=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$(openssl rand -base64 24)
# 2. Create Sealed Secret (replace plaintext values before sealing)
kubectl create secret generic formbricks-secrets \
--from-literal=DATABASE_URL="postgresql://formbricks:${POSTGRES_PASSWORD}@formbricks-postgres:5432/formbricks" \
--from-literal=NEXTAUTH_SECRET="${NEXTAUTH_SECRET}" \
--from-literal=ENCRYPTION_KEY="${ENCRYPTION_KEY}" \
--from-literal=SMTP_PASSWORD="<smtp-password>" \
-n formbricks --dry-run=client -o yaml \
| kubeseal --controller-namespace kube-system -o yaml \
> cadensa-k8s/k8s/formbricks/formbricks-sealed-secret.yaml
# 3. Apply manifests
kubectl apply -f cadensa-k8s/k8s/formbricks/namespace.yaml
kubectl apply -f cadensa-k8s/k8s/formbricks/postgres-statefulset.yaml
kubectl apply -f cadensa-k8s/k8s/formbricks/formbricks-configmap.yaml
kubectl apply -f cadensa-k8s/k8s/formbricks/formbricks-sealed-secret.yaml
kubectl apply -f cadensa-k8s/k8s/formbricks/formbricks-deployment.yaml
kubectl apply -f cadensa-k8s/k8s/formbricks/formbricks-ingress.yaml
NEXTAUTH_SECRET and ENCRYPTION_KEY must never change after the first deployment — all survey data is encrypted with these keys. Back them up securely.
Upgrading
# Edit formbricks-deployment.yaml — update the image tag
# image: formbricks/formbricks:v2.x.y
kubectl apply -f cadensa-k8s/k8s/formbricks/formbricks-deployment.yaml
Docker Compose (local / staging)
For local testing or staging environments:
# 1. Copy and fill in the env file
cp cadensa-k8s/.env.formbricks.example cadensa-k8s/.env.formbricks
# Edit .env.formbricks — set all CHANGE_ME values
# 2. Start
docker compose -f cadensa-k8s/docker-compose.formbricks.yml \
--env-file cadensa-k8s/.env.formbricks up -d
# Formbricks UI available at http://localhost:3001
Application configuration
After first deploy, open https://surveys.cadensa.io and complete the setup wizard:
- Create the admin account
- Note the Environment ID shown in Settings → Developer → Environment
- Update environment variables in both apps:
cadensa-frontend (.env):
VITE_FORMBRICKS_ENV_ID=<environment-id-from-formbricks>
VITE_FORMBRICKS_APP_URL=https://surveys.cadensa.io
cadensa-landing (.env.local):
NEXT_PUBLIC_FORMBRICKS_ENV_ID=<environment-id-from-formbricks>
NEXT_PUBLIC_FORMBRICKS_APP_URL=https://surveys.cadensa.io
How the SDK integration works
Both apps initialise the Formbricks JS SDK conditionally:
- cadensa-frontend (
src/main.tsx): initialised only inPRODmode and only after the user grantsanalyticsconsent - cadensa-landing (
components/layout/FormbricksProvider.tsx): initialised afterrequestIdleCallbackto avoid competing with LCP resources
The feedback button (FeedbackButton.tsx) tracks a feedback-button-clicked event. If the SDK is not loaded, the button falls back to a mailto:feedback@cadensa.io link.
Telemetry
TELEMETRY_DISABLED=1 is set in the ConfigMap — Formbricks does not send any usage data to the Formbricks cloud.
Data & GDPR
- Data location: Hetzner, Germany (EU)
- No US data transfer (unlike Formbricks Cloud)
- Survey responses stored in PostgreSQL — encrypted at rest (Hetzner volume encryption)
- Data subject requests: handled via Formbricks admin UI → People → Delete person
- Retention: configure survey response retention in Formbricks Settings
Related docs: Data & Security FAQ