Compare commits

..

7 Commits

Author SHA1 Message Date
Cursor Agent
0799cb81c0 Refactor: Clean up database migrations and remove deprecated fields
Co-authored-by: mail <mail@matti.sh>
2025-11-04 13:52:44 +00:00
Dhruwang Jariwala
600b793641 chore: recalibrate survey editor width to 2/3 editor and 1/3 preview (#6772) 2025-11-04 09:10:31 +00:00
Dhruwang Jariwala
cde03b6997 fix: duplicate survey issue (#6774) 2025-11-04 08:19:25 +00:00
Anshuman Pandey
00371bfb01 docs: minio intructions for docker setup (#6773)
Co-authored-by: Akhilesh Patidar <akhileshpatidar989368@gmail.com>
Co-authored-by: Akhilesh <126186908+Akhileshait@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-04 06:23:05 +00:00
Johannes
6be6782531 docs: improve API docs for better DX (#6760)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-10-31 11:59:40 +00:00
Pyrrian
3ae4f8aa68 fix: nindent typo in securityContext helm chart (#6753) 2025-10-31 12:35:20 +01:00
Thomas Brugman
3d3c69a92b feat: Add Dutch language support. (#6737) 2025-10-31 12:35:08 +01:00
14 changed files with 625 additions and 201 deletions

View File

@@ -96,7 +96,6 @@ export const SurveyAnalysisCTA = ({
const duplicateSurveyAndRoute = async (surveyId: string) => {
setLoading(true);
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
environmentId: environment.id,
surveyId: surveyId,
targetEnvironmentId: environment.id,
});

View File

@@ -65,7 +65,7 @@ export const SurveyEditorTabs = ({
let tabsToDisplay = isCxMode ? tabsComputed.filter((tab) => tab.id !== "settings") : tabsComputed;
return (
<div className="fixed z-30 flex h-12 w-full items-center justify-center border-b bg-white md:w-1/2">
<div className="fixed z-30 flex h-12 w-full items-center justify-center border-b bg-white md:w-2/3">
<nav className="flex h-full items-center space-x-4" aria-label="Tabs">
{tabsToDisplay.map((tab) => (
<button

View File

@@ -176,7 +176,7 @@ export const SurveyEditor = ({
/>
<div className="relative z-0 flex flex-1 overflow-hidden">
<main
className="relative z-0 w-1/2 flex-1 overflow-y-auto bg-slate-50 focus:outline-none"
className="relative z-0 w-full overflow-y-auto bg-slate-50 focus:outline-none md:w-2/3"
ref={surveyEditorRef}>
<SurveyEditorTabs
activeId={activeView}
@@ -260,7 +260,7 @@ export const SurveyEditor = ({
)}
</main>
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 shadow-inner md:flex md:flex-col">
<aside className="group hidden w-1/3 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 shadow-inner md:flex md:flex-col">
<PreviewSurvey
survey={localSurvey}
questionId={activeQuestionId}

View File

@@ -1,12 +1,13 @@
"use server";
import { z } from "zod";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ZSurveyFilterCriteria } from "@formbricks/types/surveys/types";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
import {
getEnvironmentIdFromSurveyId,
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromSurveyId,
getProjectIdFromEnvironmentId,
@@ -50,7 +51,6 @@ export const getSurveyAction = authenticatedActionClient
});
const ZCopySurveyToOtherEnvironmentAction = z.object({
environmentId: z.string().cuid2(),
surveyId: z.string().cuid2(),
targetEnvironmentId: z.string().cuid2(),
});
@@ -66,9 +66,10 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
parsedInput,
}: {
ctx: AuthenticatedActionClientCtx;
parsedInput: Record<string, any>;
parsedInput: z.infer<typeof ZCopySurveyToOtherEnvironmentAction>;
}) => {
const sourceEnvironmentProjectId = await getProjectIdIfEnvironmentExists(parsedInput.environmentId);
const sourceEnvironmentId = await getEnvironmentIdFromSurveyId(parsedInput.surveyId);
const sourceEnvironmentProjectId = await getProjectIdIfEnvironmentExists(sourceEnvironmentId);
const targetEnvironmentProjectId = await getProjectIdIfEnvironmentExists(
parsedInput.targetEnvironmentId
);
@@ -76,13 +77,25 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
if (!sourceEnvironmentProjectId || !targetEnvironmentProjectId) {
throw new ResourceNotFoundError(
"Environment",
sourceEnvironmentProjectId ? parsedInput.targetEnvironmentId : parsedInput.environmentId
sourceEnvironmentProjectId ? parsedInput.targetEnvironmentId : sourceEnvironmentId
);
}
const sourceEnvironmentOrganizationId = await getOrganizationIdFromEnvironmentId(sourceEnvironmentId);
const targetEnvironmentOrganizationId = await getOrganizationIdFromEnvironmentId(
parsedInput.targetEnvironmentId
);
if (sourceEnvironmentOrganizationId !== targetEnvironmentOrganizationId) {
throw new OperationNotAllowedError(
"Source and target environments must be in the same organization"
);
}
// authorization check for source environment
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
organizationId: sourceEnvironmentOrganizationId,
access: [
{
type: "organization",
@@ -96,9 +109,10 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
],
});
// authorization check for target environment
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
organizationId: targetEnvironmentOrganizationId,
access: [
{
type: "organization",
@@ -112,12 +126,10 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
],
});
ctx.auditLoggingCtx.organizationId = await getOrganizationIdFromEnvironmentId(
parsedInput.environmentId
);
ctx.auditLoggingCtx.organizationId = sourceEnvironmentOrganizationId;
ctx.auditLoggingCtx.surveyId = parsedInput.surveyId;
const result = await copySurveyToOtherEnvironment(
parsedInput.environmentId,
sourceEnvironmentId,
parsedInput.surveyId,
parsedInput.targetEnvironmentId,
ctx.user.id

View File

@@ -129,7 +129,6 @@ export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: C
return {
operation: copySurveyToOtherEnvironmentAction({
environmentId: survey.environmentId,
surveyId: survey.id,
targetEnvironmentId: environmentId,
}),

View File

@@ -113,7 +113,6 @@ export const SurveyDropDownMenu = ({
setLoading(true);
try {
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
environmentId,
surveyId,
targetEnvironmentId: environmentId,
});

View File

@@ -167,7 +167,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
<div
ref={ContentRef}
data-testid="mobile-preview-container"
className={`relative h-[90%] w-[45%] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}>
className={`relative h-[90%] w-full overflow-hidden rounded-[3rem] border-[6px] border-slate-400 lg:w-[75%] ${getFilterStyle()}`}>
{/* below element is use to create notch for the mobile device mockup */}
<div className="absolute left-1/2 right-1/2 top-2 z-20 h-4 w-1/3 -translate-x-1/2 transform rounded-full bg-slate-400"></div>
{surveyType === "link" && renderBackground()}

View File

@@ -1238,21 +1238,45 @@
"requestBody": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"data": {
"hs8yd14l9h8u353tjmv6rzaw": "clicked",
"tcgls0063n8ri7dtrbnepcmz": "Who? Who? Who?"
},
"finished": false,
"meta": {
"action": "test action",
"source": "Postman API",
"url": "https://postman.com"
},
"surveyId": "{{surveyId}}",
"userId": "{{userId}}"
}
}
},
"schema": {
"example": {
"data": {
"hs8yd14l9h8u353tjmv6rzaw": "clicked",
"tcgls0063n8ri7dtrbnepcmz": "Who? Who? Who?"
"properties": {
"data": { "additionalProperties": true, "type": "object" },
"finished": { "type": "boolean" },
"language": {
"description": "Language of the response (survey should have this language enabled)",
"enum": ["en", "de", "pt", "etc.."],
"type": "string"
},
"finished": false,
"meta": {
"action": "test action",
"source": "Postman API",
"url": "https://postman.com"
"properties": {
"action": { "type": "string" },
"source": { "type": "string" },
"url": { "type": "string" }
},
"type": "object"
},
"surveyId": "{{surveyId}}",
"userId": "{{userId}} (optional)"
"surveyId": { "type": "string" },
"userId": { "type": "string" }
},
"required": ["surveyId"],
"type": "object"
}
}
@@ -2361,20 +2385,55 @@
},
"noCodeConfig": {
"description": "Configuration object required when type is 'noCode'. Defines the conditions for triggering the action. Not needed for 'code' type.",
"example": {
"elementSelector": {
"cssSelector": ".button-class",
"innerHtml": "Click me"
},
"type": "click",
"urlFilters": [
{
"rule": "contains",
"value": "https://www.google.com"
}
]
},
"nullable": true,
"properties": {
"elementSelector": {
"description": "Element selector (required for click type)",
"properties": {
"cssSelector": {
"description": "CSS selector for the element",
"type": "string"
},
"innerHtml": {
"description": "Inner HTML text to match",
"type": "string"
}
},
"type": "object"
},
"type": {
"description": "Type of no-code trigger",
"enum": ["click", "pageView", "exitIntent", "fiftyPercentScroll"],
"type": "string"
},
"urlFilters": {
"items": {
"properties": {
"rule": {
"description": "URL matching rule",
"enum": [
"exactMatch",
"contains",
"startsWith",
"endsWith",
"notMatch",
"notContains",
"matchesRegex"
],
"type": "string"
},
"value": {
"description": "URL pattern to match",
"type": "string"
}
},
"required": ["rule", "value"],
"type": "object"
},
"type": "array"
}
},
"required": ["type", "urlFilters"],
"type": "object"
},
"type": {
@@ -5021,17 +5080,52 @@
"requestBody": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"createdAt": "2024-09-05T08:44:16.051Z",
"data": {
"hg508afs7lgx8nlni5dtit5u": ["Hello World"]
},
"finished": false,
"language": "en",
"surveyId": "clwj7hi7r0000vfhpfze6vjdg",
"updatedAt": "2024-09-05T08:44:16.051Z"
}
}
},
"schema": {
"example": {
"createdAt": "2024-09-05T08:44:16.051Z",
"data": {
"hg508afs7lgx8nlni5dtit5u": ["Hello World"]
"properties": {
"createdAt": {
"description": "Creation timestamp (optional; usually set by server)",
"format": "date-time",
"type": "string"
},
"finished": false,
"language": "default",
"surveyId": "clwj7hi7r0000vfhpfze6vjdg",
"updatedAt": "2024-09-05T08:44:16.051Z"
"data": {
"additionalProperties": true,
"description": "Answers keyed by questionId; value shape depends on question type",
"type": "object"
},
"finished": {
"description": "Whether the response is marked as finished",
"type": "boolean"
},
"language": {
"description": "Language of the response (survey should have this language enabled)",
"enum": ["en", "de", "pt", "etc.."],
"type": "string"
},
"surveyId": {
"description": "The ID of the survey this response belongs to",
"type": "string"
},
"updatedAt": {
"description": "Update timestamp (optional; usually set by server)",
"format": "date-time",
"type": "string"
}
},
"required": ["surveyId"],
"type": "object"
}
}
@@ -5768,6 +5862,32 @@
"allowedFileExtensions": {
"description": "Optional. List of allowed file extensions.",
"items": {
"enum": [
"heic",
"png",
"jpeg",
"jpg",
"webp",
"pdf",
"eml",
"doc",
"docx",
"xls",
"xlsx",
"ppt",
"pptx",
"txt",
"csv",
"mp4",
"mov",
"avi",
"mkv",
"webm",
"zip",
"rar",
"7z",
"tar"
],
"type": "string"
},
"type": "array"
@@ -6108,113 +6228,153 @@
"requestBody": {
"content": {
"application/json": {
"schema": {
"example": {
"autoClose": null,
"autoComplete": null,
"createdBy": null,
"delay": 0,
"displayLimit": null,
"displayOption": "displayOnce",
"displayPercentage": null,
"endings": [
{
"buttonLabel": {
"default": "Create your own Survey"
"examples": {
"default": {
"value": {
"autoClose": null,
"autoComplete": null,
"createdBy": null,
"delay": 0,
"displayLimit": null,
"displayOption": "displayOnce",
"displayPercentage": null,
"endings": [
{
"buttonLabel": { "default": "Create your own Survey" },
"buttonLink": "https://formbricks.com/signup",
"headline": { "default": "Thank you!" },
"id": "p73t62dgwq0cvmtt6ug0hmfc",
"subheader": { "default": "We appreciate your feedback." },
"type": "endScreen"
}
],
"environmentId": "{{environmentId}}",
"hiddenFields": { "enabled": false, "fieldIds": [] },
"isVerifyEmailEnabled": false,
"languages": [],
"name": "Example Survey",
"pin": null,
"productOverwrites": null,
"questions": [
{
"headline": { "default": "What would you like to know?" },
"id": "ovpy6va1hab7fl12n913zua0",
"inputType": "text",
"placeholder": { "default": "Type your answer here..." },
"required": true,
"subheader": { "default": "This is an example survey." },
"type": "openText"
},
"buttonLink": "https://formbricks.com/signup",
"headline": {
"default": "Thank you!"
{
"choices": [
{ "id": "xpoxuu3sifk1ee8he67ctf5i", "label": { "default": "Sun \u2600\ufe0f" } },
{ "id": "hnsovcdmxtcbly6tig1az3qc", "label": { "default": "Ocean \ud83c\udf0a" } },
{ "id": "kcnelzdxknvwo8fq20d3nrr5", "label": { "default": "Palms \ud83c\udf34" } }
],
"headline": { "default": "What's important on vacay?" },
"id": "awkn2llljy7a4oulp5t15yec",
"required": true,
"shuffleOption": "none",
"type": "multipleChoiceMulti"
}
],
"recontactDays": null,
"redirectUrl": null,
"segmentId": null,
"showLanguageSwitch": null,
"singleUse": { "enabled": false, "isEncrypted": true },
"status": "inProgress",
"styling": null,
"surveyClosedMessage": null,
"triggers": [],
"type": "link",
"welcomeCard": {
"enabled": true,
"fileUrl": "",
"headline": { "default": "Welcome!" },
"html": {
"default": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Thanks for providing your feedback - let's go!</span></p>"
},
"id": "p73t62dgwq0cvmtt6ug0hmfc",
"subheader": {
"default": "We appreciate your feedback."
},
"type": "endScreen"
"showResponseCount": false,
"timeToFinish": false
}
],
"environmentId": "{{environmentId}}",
"hiddenFields": {
"enabled": false,
"fieldIds": []
},
"isVerifyEmailEnabled": false,
"languages": [],
"name": "Example Survey",
"pin": null,
"productOverwrites": null,
"questions": [
{
"headline": {
"default": "What would you like to know?"
},
"id": "ovpy6va1hab7fl12n913zua0",
"inputType": "text",
"placeholder": {
"default": "Type your answer here..."
},
"required": true,
"subheader": {
"default": "This is an example survey."
},
"type": "openText"
},
{
"choices": [
{
"id": "xpoxuu3sifk1ee8he67ctf5i",
"label": {
"default": "Sun \u2600\ufe0f"
}
},
{
"id": "hnsovcdmxtcbly6tig1az3qc",
"label": {
"default": "Ocean \ud83c\udf0a"
}
},
{
"id": "kcnelzdxknvwo8fq20d3nrr5",
"label": {
"default": "Palms \ud83c\udf34"
}
}
],
"headline": {
"default": "What's important on vacay?"
},
"id": "awkn2llljy7a4oulp5t15yec",
"required": true,
"shuffleOption": "none",
"type": "multipleChoiceMulti"
}
],
"recontactDays": null,
"redirectUrl": null,
"segmentId": null,
"showLanguageSwitch": null,
"singleUse": {
"enabled": false,
"isEncrypted": true
},
"status": "inProgress",
"styling": null,
"surveyClosedMessage": null,
"triggers": [],
"type": "link",
"welcomeCard": {
"enabled": true,
"fileUrl": "",
"headline": {
"default": "Welcome!"
},
"html": {
"default": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Thanks for providing your feedback - let's go!</span></p>"
},
"showResponseCount": false,
"timeToFinish": false
}
}
},
"schema": {
"properties": {
"displayOption": {
"enum": ["displayOnce", "displayMultiple", "respondMultiple", "displaySome"],
"type": "string"
},
"environmentId": { "type": "string" },
"languages": {
"items": {
"properties": {
"default": { "type": "boolean" },
"enabled": { "type": "boolean" },
"language": {
"description": "Language of the survey (This language should exist in your environment)",
"enum": ["en", "de", "pt", "etc.."],
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"name": { "type": "string" },
"questions": {
"items": {
"properties": {
"date": {
"properties": {
"format": { "enum": ["M-d-y", "d-M-y", "y-M-d"], "type": "string" }
},
"type": "object"
},
"id": { "type": "string" },
"inputType": {
"enum": ["text", "email", "url", "number", "phone"],
"type": "string"
},
"rating": {
"properties": {
"range": { "enum": [3, 4, 5, 6, 7, 10], "type": "integer" },
"scale": { "enum": ["number", "smiley", "star"], "type": "string" }
},
"type": "object"
},
"shuffleOption": { "enum": ["none", "all", "exceptLast"], "type": "string" },
"type": {
"enum": [
"address",
"cta",
"consent",
"date",
"fileUpload",
"matrix",
"multipleChoiceMulti",
"multipleChoiceSingle",
"nps",
"openText",
"pictureSelection",
"rating",
"cal",
"ranking",
"contactInfo"
],
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"status": { "enum": ["draft", "inProgress", "paused", "completed"], "type": "string" },
"type": { "enum": ["link", "app"], "type": "string" }
},
"required": ["environmentId", "name", "type", "status"],
"type": "object"
}
}
@@ -7468,11 +7628,41 @@
"requestBody": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"triggers": ["responseCreated", "responseUpdated", "responseFinished"],
"url": "https://eoy8o887lmsqmhz.m.pipedream.net"
}
}
},
"schema": {
"example": {
"triggers": ["responseCreated", "responseUpdated", "responseFinished"],
"url": "https://eoy8o887lmsqmhz.m.pipedream.net"
"properties": {
"name": {
"description": "Optional name for the webhook",
"type": "string"
},
"surveyIds": {
"description": "Optional list of survey IDs to filter webhook calls",
"items": {
"type": "string"
},
"type": "array"
},
"triggers": {
"description": "List of events that trigger this webhook",
"items": {
"enum": ["responseCreated", "responseUpdated", "responseFinished"],
"type": "string"
},
"type": "array"
},
"url": {
"description": "The webhook URL to call when triggers are fired",
"type": "string"
}
},
"required": ["url", "triggers"],
"type": "object"
}
}
@@ -7947,7 +8137,7 @@
},
"servers": [
{
"url": "http://{{baseurl}}",
"url": "https://{baseurl}",
"variables": {
"baseurl": {
"default": "localhost:3000"

View File

@@ -97,10 +97,44 @@ paths:
content:
application/json:
schema:
type: object
properties:
surveyId:
type: string
description: The ID of the survey this response belongs to
responses:
type: object
additionalProperties: true
description: Answers keyed by questionId; value shape depends on question type
finished:
type: boolean
description: Whether the response is marked as finished
language:
type: string
enum:
[
"en-US",
"de-DE",
"pt-BR",
"fr-FR",
"zh-Hant-TW",
"pt-PT",
"ro-RO",
"ja-JP",
"zh-Hans-CN",
]
description: Locale of the response
meta:
type: object
properties:
action: { type: string }
source: { type: string }
url: { type: string }
description: Optional metadata about the response
required: ["surveyId"]
example:
surveyId: survey123
responses: {}
type: object
responses:
"201":
content:
@@ -136,9 +170,15 @@ paths:
content:
application/json:
schema:
type: object
properties:
attributes:
type: object
additionalProperties: true
description: Key-value pairs of contact attributes
required: ["attributes"]
example:
attributes: {}
type: object
responses:
"200":
content:

View File

@@ -117,6 +117,191 @@ Please take a look at our [migration guide](/self-hosting/advanced/migration) fo
docker compose up -d
```
## Optional: Adding MinIO for File Storage
MinIO provides S3-compatible object storage for file uploads in Formbricks. If you want to enable features like image uploads, file uploads in surveys, or custom logos, you can add MinIO to your Docker setup.
<Note>
For detailed information about file storage options and configuration, see our [File Uploads
Configuration](/self-hosting/configuration/file-uploads) guide.
</Note>
<Warning>
**For production deployments with HTTPS**, use the [one-click setup script](/self-hosting/setup/one-click)
which automatically configures MinIO with Traefik, SSL certificates, and a subdomain (required for MinIO in
production). The setups below are suitable for local development or testing only.
</Warning>
### Quick Start: Using docker-compose.dev.yml
The fastest way to test MinIO with Formbricks is to use the included `docker-compose.dev.yml` which already has MinIO pre-configured.
1. **Start MinIO and Services**
From the repository root:
```bash
docker compose -f docker-compose.dev.yml up -d
```
This starts PostgreSQL, Valkey (Redis), MinIO, and Mailhog.
2. **Access MinIO Console**
Open http://localhost:9001 in your browser.
Login credentials:
- Username: `devminio`
- Password: `devminio123`
3. **Create Bucket**
- Click "Buckets" in the left sidebar
- Click "Create Bucket"
- Name it: `formbricks`
4. **Configure Formbricks**
Update your `.env` file or environment variables with MinIO configuration:
```bash
# MinIO S3 Storage
S3_ACCESS_KEY="devminio"
S3_SECRET_KEY="devminio123"
S3_REGION="us-east-1"
S3_BUCKET_NAME="formbricks"
S3_ENDPOINT_URL="http://localhost:9000"
S3_FORCE_PATH_STYLE="1"
```
5. **Verify in MinIO Console**
After uploading files in Formbricks, view them at http://localhost:9001:
- Navigate to Buckets → formbricks → Browse
- Your uploaded files will appear here
<Note>
The `docker-compose.dev.yml` file includes MinIO with console access on port 9001, making it easy to
visually verify file uploads. This is the recommended approach for development and testing.
</Note>
### Manual MinIO Setup (Custom Configuration)
<Note>
<strong>Recommended:</strong> If you can, use <code>docker-compose.dev.yml</code> for the fastest path. Use
this manual approach only when you need to integrate MinIO into an existing <code>docker-compose.yml</code>{" "}
or customize settings.
</Note>
If you prefer to add MinIO to your own `docker-compose.yml`, follow these steps:
1. **Add the MinIO service**
Add this service alongside your existing `formbricks` and `postgres` services:
```yaml
services:
# ... your existing services (formbricks, postgres, redis/valkey, etc.)
minio:
image: minio/minio:latest
restart: always
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: "formbricks-root"
MINIO_ROOT_PASSWORD: "change-this-secure-password"
ports:
- "9000:9000" # S3 API
- "9001:9001" # Web console
volumes:
- minio-data:/data
```
<Note>
For production pinning, consider using a digest (e.g., <code>minio/minio@sha256:...</code>) and review
periodically with <code>docker inspect minio/minio:latest</code>.
</Note>
2. **Declare the MinIO volume**
Add (or extend) your `volumes` block:
```yaml
volumes:
postgres:
driver: local
redis:
driver: local
minio-data:
driver: local
```
3. **Start services**
```bash
docker compose up -d
```
4. **Open the MinIO Console & Create a Bucket**
- Visit **http://localhost:9001**
- Log in with:
- **Username:** `formbricks-root`
- **Password:** `change-this-secure-password`
- Go to **Buckets → Create Bucket**
- Name it: **`formbricks`**
5. **Configure Formbricks to use MinIO**
In your `.env` or `formbricks` service environment, set:
```bash
# MinIO S3 Storage
S3_ACCESS_KEY="formbricks-root"
S3_SECRET_KEY="change-this-secure-password"
S3_REGION="us-east-1"
S3_BUCKET_NAME="formbricks"
S3_ENDPOINT_URL="http://minio:9000"
S3_FORCE_PATH_STYLE="1"
```
<Note>
These credentials should match <code>MINIO_ROOT_USER</code> and <code>MINIO_ROOT_PASSWORD</code> above.
For local/dev this is fine. For production, create a dedicated MinIO user with restricted policies.
</Note>
6. **Verify uploads**
After uploading a file in Formbricks, check **http://localhost:9001**:
- **Buckets → formbricks → Browse**
You should see your uploaded files.
#### Tips & Common Gotchas
- **Connection refused**: Ensure the `minio` container is running and port **9000** is reachable from the Formbricks container (use the internal URL `http://minio:9000`).
- **Bucket not found**: Create the `formbricks` bucket in the console before uploading.
- **Auth failed**: Confirm `S3_ACCESS_KEY`/`S3_SECRET_KEY` match MinIO credentials.
- **Health check**: From the Formbricks container:
```bash
docker compose exec formbricks sh -c 'wget -O- http://minio:9000/minio/health/ready'
```
### Production Setup with Traefik
For production deployments, use the [one-click setup script](/self-hosting/setup/one-click) which automatically configures:
- MinIO service with Traefik reverse proxy
- Dedicated subdomain (e.g., `files.yourdomain.com`) - **required for production**
- Automatic SSL certificate generation via Let's Encrypt
- CORS configuration for your domain
- Rate limiting middleware
- Secure credential generation
The production setup from [formbricks.sh](https://github.com/formbricks/formbricks/blob/main/docker/formbricks.sh) includes advanced features not covered in this manual setup. For production use, we strongly recommend using the one-click installer.
## Debug
If you encounter any issues, you can check the logs of the container with this command:

View File

@@ -74,7 +74,7 @@ spec:
{{- end }}
{{- if .Values.deployment.securityContext }}
securityContext:
{{ toYaml .Values.deployment.securityContext | indent 8 }}
{{ toYaml .Values.deployment.securityContext | nindent 8 }}
{{- end }}
terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds | default 30 }}
containers:

View File

@@ -8,19 +8,19 @@
*/
-- Update any scheduled surveys to paused
UPDATE "public"."Survey" SET "status" = 'paused' WHERE "status" = 'scheduled';
UPDATE "Survey" SET "status" = 'paused' WHERE "status" = 'scheduled';
-- AlterEnum
BEGIN;
CREATE TYPE "public"."SurveyStatus_new" AS ENUM ('draft', 'inProgress', 'paused', 'completed');
ALTER TABLE "public"."Survey" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "public"."Survey" ALTER COLUMN "status" TYPE "public"."SurveyStatus_new" USING ("status"::text::"public"."SurveyStatus_new");
ALTER TYPE "public"."SurveyStatus" RENAME TO "SurveyStatus_old";
ALTER TYPE "public"."SurveyStatus_new" RENAME TO "SurveyStatus";
DROP TYPE "public"."SurveyStatus_old";
ALTER TABLE "public"."Survey" ALTER COLUMN "status" SET DEFAULT 'draft';
CREATE TYPE "SurveyStatus_new" AS ENUM ('draft', 'inProgress', 'paused', 'completed');
ALTER TABLE "Survey" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "Survey" ALTER COLUMN "status" TYPE "SurveyStatus_new" USING ("status"::text::"SurveyStatus_new");
ALTER TYPE "SurveyStatus" RENAME TO "SurveyStatus_old";
ALTER TYPE "SurveyStatus_new" RENAME TO "SurveyStatus";
DROP TYPE "SurveyStatus_old";
ALTER TABLE "Survey" ALTER COLUMN "status" SET DEFAULT 'draft';
COMMIT;
-- AlterTable
ALTER TABLE "public"."Survey" DROP COLUMN "closeOnDate",
ALTER TABLE "Survey" DROP COLUMN "closeOnDate",
DROP COLUMN "runOnDate";

View File

@@ -20,88 +20,88 @@
*/
-- AlterEnum
BEGIN;
CREATE TYPE "public"."SurveyType_new" AS ENUM ('link', 'app');
ALTER TABLE "public"."Survey" ALTER COLUMN "type" DROP DEFAULT;
ALTER TABLE "public"."Survey" ALTER COLUMN "type" TYPE "public"."SurveyType_new" USING ("type"::text::"public"."SurveyType_new");
ALTER TYPE "public"."SurveyType" RENAME TO "SurveyType_old";
ALTER TYPE "public"."SurveyType_new" RENAME TO "SurveyType";
DROP TYPE "public"."SurveyType_old";
ALTER TABLE "public"."Survey" ALTER COLUMN "type" SET DEFAULT 'app';
CREATE TYPE "SurveyType_new" AS ENUM ('link', 'app');
ALTER TABLE "Survey" ALTER COLUMN "type" DROP DEFAULT;
ALTER TABLE "Survey" ALTER COLUMN "type" TYPE "SurveyType_new" USING ("type"::text::"SurveyType_new");
ALTER TYPE "SurveyType" RENAME TO "SurveyType_old";
ALTER TYPE "SurveyType_new" RENAME TO "SurveyType";
DROP TYPE "SurveyType_old";
ALTER TABLE "Survey" ALTER COLUMN "type" SET DEFAULT 'app';
COMMIT;
-- DropForeignKey
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_environmentId_fkey";
ALTER TABLE "Document" DROP CONSTRAINT "Document_environmentId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_responseId_fkey";
ALTER TABLE "Document" DROP CONSTRAINT "Document_responseId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_surveyId_fkey";
ALTER TABLE "Document" DROP CONSTRAINT "Document_surveyId_fkey";
-- DropForeignKey
ALTER TABLE "public"."DocumentInsight" DROP CONSTRAINT "DocumentInsight_documentId_fkey";
ALTER TABLE "DocumentInsight" DROP CONSTRAINT "DocumentInsight_documentId_fkey";
-- DropForeignKey
ALTER TABLE "public"."DocumentInsight" DROP CONSTRAINT "DocumentInsight_insightId_fkey";
ALTER TABLE "DocumentInsight" DROP CONSTRAINT "DocumentInsight_insightId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Insight" DROP CONSTRAINT "Insight_environmentId_fkey";
ALTER TABLE "Insight" DROP CONSTRAINT "Insight_environmentId_fkey";
-- DropIndex
DROP INDEX "public"."Display_responseId_key";
DROP INDEX "Display_responseId_key";
-- AlterTable
ALTER TABLE "public"."Display" DROP COLUMN "responseId",
ALTER TABLE "Display" DROP COLUMN "responseId",
DROP COLUMN "status";
-- AlterTable
ALTER TABLE "public"."Environment" DROP COLUMN "widgetSetupCompleted";
ALTER TABLE "Environment" DROP COLUMN "widgetSetupCompleted";
-- AlterTable
ALTER TABLE "public"."Invite" DROP COLUMN "deprecatedRole";
ALTER TABLE "Invite" DROP COLUMN "deprecatedRole";
-- AlterTable
ALTER TABLE "public"."Membership" DROP COLUMN "deprecatedRole";
ALTER TABLE "Membership" DROP COLUMN "deprecatedRole";
-- AlterTable
ALTER TABLE "public"."Project" DROP COLUMN "brandColor",
ALTER TABLE "Project" DROP COLUMN "brandColor",
DROP COLUMN "highlightBorderColor";
-- AlterTable
ALTER TABLE "public"."Survey" DROP COLUMN "thankYouCard",
ALTER TABLE "Survey" DROP COLUMN "thankYouCard",
DROP COLUMN "verifyEmail",
ALTER COLUMN "type" SET DEFAULT 'app';
-- AlterTable
ALTER TABLE "public"."User" DROP COLUMN "objective",
ALTER TABLE "User" DROP COLUMN "objective",
DROP COLUMN "role";
-- DropTable
DROP TABLE "public"."Document";
DROP TABLE "Document";
-- DropTable
DROP TABLE "public"."DocumentInsight";
DROP TABLE "DocumentInsight";
-- DropTable
DROP TABLE "public"."Insight";
DROP TABLE "Insight";
-- DropEnum
DROP TYPE "public"."DisplayStatus";
DROP TYPE "DisplayStatus";
-- DropEnum
DROP TYPE "public"."InsightCategory";
DROP TYPE "InsightCategory";
-- DropEnum
DROP TYPE "public"."Intention";
DROP TYPE "Intention";
-- DropEnum
DROP TYPE "public"."MembershipRole";
DROP TYPE "MembershipRole";
-- DropEnum
DROP TYPE "public"."Objective";
DROP TYPE "Objective";
-- DropEnum
DROP TYPE "public"."Role";
DROP TYPE "Role";
-- DropEnum
DROP TYPE "public"."Sentiment";
DROP TYPE "Sentiment";

View File

@@ -1,9 +1,9 @@
-- DropIndex
DROP INDEX IF EXISTS "public"."ApiKey_hashedKey_key";
DROP INDEX IF EXISTS "ApiKey_hashedKey_key";
-- AlterTable
ALTER TABLE "public"."ApiKey" ADD COLUMN IF NOT EXISTS "lookupHash" TEXT;
ALTER TABLE "ApiKey" ADD COLUMN IF NOT EXISTS "lookupHash" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "ApiKey_lookupHash_key" ON "public"."ApiKey"("lookupHash");
CREATE UNIQUE INDEX IF NOT EXISTS "ApiKey_lookupHash_key" ON "ApiKey"("lookupHash");