diff --git a/.env.example b/.env.example
index 664abd3bf7..b3ed82c802 100644
--- a/.env.example
+++ b/.env.example
@@ -210,6 +210,8 @@ UNKEY_ROOT_KEY=
# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin.
# It's used automatically by Sentry during the build for authentication when uploading source maps.
# SENTRY_AUTH_TOKEN=
+# The SENTRY_ENVIRONMENT is the environment which the error will belong to in the Sentry dashboard
+# SENTRY_ENVIRONMENT=
# Configure the minimum role for user management from UI(owner, manager, disabled)
# USER_MANAGEMENT_MINIMUM_ROLE="manager"
diff --git a/.github/actions/upload-sentry-sourcemaps/action.yml b/.github/actions/upload-sentry-sourcemaps/action.yml
new file mode 100644
index 0000000000..e8510aa2f2
--- /dev/null
+++ b/.github/actions/upload-sentry-sourcemaps/action.yml
@@ -0,0 +1,121 @@
+name: 'Upload Sentry Sourcemaps'
+description: 'Extract sourcemaps from Docker image and upload to Sentry'
+
+inputs:
+ docker_image:
+ description: 'Docker image to extract sourcemaps from'
+ required: true
+ release_version:
+ description: 'Sentry release version (e.g., v1.2.3)'
+ required: true
+ sentry_auth_token:
+ description: 'Sentry authentication token'
+ required: true
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Validate Sentry auth token
+ shell: bash
+ run: |
+ set -euo pipefail
+ echo "๐ Validating Sentry authentication token..."
+
+ # Assign token to local variable for secure handling
+ SENTRY_TOKEN="${{ inputs.sentry_auth_token }}"
+
+ # Test the token by making a simple API call to Sentry
+ response=$(curl -s -w "%{http_code}" -o /tmp/sentry_response.json \
+ -H "Authorization: Bearer $SENTRY_TOKEN" \
+ "https://sentry.io/api/0/organizations/formbricks/")
+
+ http_code=$(echo "$response" | tail -n1)
+
+ if [ "$http_code" != "200" ]; then
+ echo "โ Error: Invalid Sentry auth token (HTTP $http_code)"
+ echo "Please check your SENTRY_AUTH_TOKEN is correct and has the necessary permissions."
+ if [ -f /tmp/sentry_response.json ]; then
+ echo "Response body:"
+ cat /tmp/sentry_response.json
+ fi
+ exit 1
+ fi
+
+ echo "โ
Sentry auth token validated successfully"
+
+ # Clean up temp file
+ rm -f /tmp/sentry_response.json
+
+ - name: Extract sourcemaps from Docker image
+ shell: bash
+ run: |
+ set -euo pipefail
+ echo "๐ฆ Extracting sourcemaps from Docker image: ${{ inputs.docker_image }}"
+
+ # Create temporary container from the image and capture its ID
+ echo "Creating temporary container..."
+ CONTAINER_ID=$(docker create "${{ inputs.docker_image }}")
+ echo "Container created with ID: $CONTAINER_ID"
+
+ # Set up cleanup function to ensure container is removed on script exit
+ cleanup_container() {
+ # Capture the current exit code to preserve it
+ local original_exit_code=$?
+
+ echo "๐งน Cleaning up Docker container..."
+
+ # Remove the container if it exists (ignore errors if already removed)
+ if [ -n "$CONTAINER_ID" ]; then
+ docker rm -f "$CONTAINER_ID" 2>/dev/null || true
+ echo "Container $CONTAINER_ID removed"
+ fi
+
+ # Exit with the original exit code to preserve script success/failure status
+ exit $original_exit_code
+ }
+
+ # Register cleanup function to run on script exit (success or failure)
+ trap cleanup_container EXIT
+
+ # Extract .next directory containing sourcemaps
+ docker cp "$CONTAINER_ID:/home/nextjs/apps/web/.next" ./extracted-next
+
+ # Verify sourcemaps exist
+ if [ ! -d "./extracted-next/static/chunks" ]; then
+ echo "โ Error: .next/static/chunks directory not found in Docker image"
+ echo "Expected structure: /home/nextjs/apps/web/.next/static/chunks/"
+ exit 1
+ fi
+
+ sourcemap_count=$(find ./extracted-next/static/chunks -name "*.map" | wc -l)
+ echo "โ
Found $sourcemap_count sourcemap files"
+
+ if [ "$sourcemap_count" -eq 0 ]; then
+ echo "โ Error: No sourcemap files found. Check that productionBrowserSourceMaps is enabled."
+ exit 1
+ fi
+
+ - name: Create Sentry release and upload sourcemaps
+ uses: getsentry/action-release@v3
+ env:
+ SENTRY_AUTH_TOKEN: ${{ inputs.sentry_auth_token }}
+ SENTRY_ORG: formbricks
+ SENTRY_PROJECT: formbricks-cloud
+ with:
+ environment: production
+ version: ${{ inputs.release_version }}
+ sourcemaps: './extracted-next/'
+
+ - name: Clean up extracted files
+ shell: bash
+ if: always()
+ run: |
+ set -euo pipefail
+ # Clean up extracted files
+ rm -rf ./extracted-next
+ echo "๐งน Cleaned up extracted files"
diff --git a/.github/workflows/formbricks-release.yml b/.github/workflows/formbricks-release.yml
index 68f45a88b5..6df33e7dd8 100644
--- a/.github/workflows/formbricks-release.yml
+++ b/.github/workflows/formbricks-release.yml
@@ -32,3 +32,25 @@ jobs:
with:
VERSION: v${{ needs.docker-build.outputs.VERSION }}
ENVIRONMENT: "prod"
+
+ upload-sentry-sourcemaps:
+ name: Upload Sentry Sourcemaps
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ needs:
+ - docker-build
+ - deploy-formbricks-cloud
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4.2.2
+ with:
+ fetch-depth: 0
+
+ - name: Upload Sentry Sourcemaps
+ uses: ./.github/actions/upload-sentry-sourcemaps
+ continue-on-error: true
+ with:
+ docker_image: ghcr.io/formbricks/formbricks:v${{ needs.docker-build.outputs.VERSION }}
+ release_version: v${{ needs.docker-build.outputs.VERSION }}
+ sentry_auth_token: ${{ secrets.SENTRY_AUTH_TOKEN }}
diff --git a/.github/workflows/upload-sentry-sourcemaps.yml b/.github/workflows/upload-sentry-sourcemaps.yml
new file mode 100644
index 0000000000..7af92ebc10
--- /dev/null
+++ b/.github/workflows/upload-sentry-sourcemaps.yml
@@ -0,0 +1,46 @@
+name: Upload Sentry Sourcemaps (Manual)
+
+on:
+ workflow_dispatch:
+ inputs:
+ docker_image:
+ description: "Docker image to extract sourcemaps from"
+ required: true
+ type: string
+ release_version:
+ description: "Release version (e.g., v1.2.3)"
+ required: true
+ type: string
+ tag_version:
+ description: "Docker image tag (leave empty to use release_version)"
+ required: false
+ type: string
+
+permissions:
+ contents: read
+
+jobs:
+ upload-sourcemaps:
+ name: Upload Sourcemaps to Sentry
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4.2.2
+ with:
+ fetch-depth: 0
+
+ - name: Set Docker Image
+ run: |
+ if [ -n "${{ inputs.tag_version }}" ]; then
+ echo "DOCKER_IMAGE=${{ inputs.docker_image }}:${{ inputs.tag_version }}" >> $GITHUB_ENV
+ else
+ echo "DOCKER_IMAGE=${{ inputs.docker_image }}:${{ inputs.release_version }}" >> $GITHUB_ENV
+ fi
+
+ - name: Upload Sourcemaps to Sentry
+ uses: ./.github/actions/upload-sentry-sourcemaps
+ with:
+ docker_image: ${{ env.DOCKER_IMAGE }}
+ release_version: ${{ inputs.release_version }}
+ sentry_auth_token: ${{ secrets.SENTRY_AUTH_TOKEN }}
\ No newline at end of file
diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile
index e9729940cf..48dadbeb2a 100644
--- a/apps/web/Dockerfile
+++ b/apps/web/Dockerfile
@@ -25,21 +25,9 @@ RUN corepack prepare pnpm@9.15.9 --activate
# Install necessary build tools and compilers
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
-# BuildKit secret handling without hardcoded fallback values
-# This approach relies entirely on secrets passed from GitHub Actions
-RUN echo '#!/bin/sh' > /tmp/read-secrets.sh && \
- echo 'if [ -f "/run/secrets/database_url" ]; then' >> /tmp/read-secrets.sh && \
- echo ' export DATABASE_URL=$(cat /run/secrets/database_url)' >> /tmp/read-secrets.sh && \
- echo 'else' >> /tmp/read-secrets.sh && \
- echo ' echo "DATABASE_URL secret not found. Build may fail if this is required."' >> /tmp/read-secrets.sh && \
- echo 'fi' >> /tmp/read-secrets.sh && \
- echo 'if [ -f "/run/secrets/encryption_key" ]; then' >> /tmp/read-secrets.sh && \
- echo ' export ENCRYPTION_KEY=$(cat /run/secrets/encryption_key)' >> /tmp/read-secrets.sh && \
- echo 'else' >> /tmp/read-secrets.sh && \
- echo ' echo "ENCRYPTION_KEY secret not found. Build may fail if this is required."' >> /tmp/read-secrets.sh && \
- echo 'fi' >> /tmp/read-secrets.sh && \
- echo 'exec "$@"' >> /tmp/read-secrets.sh && \
- chmod +x /tmp/read-secrets.sh
+# Copy the secrets handling script
+COPY apps/web/scripts/docker/read-secrets.sh /tmp/read-secrets.sh
+RUN chmod +x /tmp/read-secrets.sh
# Increase Node.js memory limit as a regular build argument
ARG NODE_OPTIONS="--max_old_space_size=4096"
@@ -62,6 +50,9 @@ RUN touch apps/web/.env
# Install the dependencies
RUN pnpm install --ignore-scripts
+# Build the database package first
+RUN pnpm build --filter=@formbricks/database
+
# Build the project using our secret reader script
# This mounts the secrets only during this build step without storing them in layers
RUN --mount=type=secret,id=database_url \
@@ -106,20 +97,8 @@ RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
-COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
-RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
-
-COPY --from=installer /app/packages/database/migration ./packages/database/migration
-RUN chown -R nextjs:nextjs ./packages/database/migration && chmod -R 755 ./packages/database/migration
-
-COPY --from=installer /app/packages/database/src ./packages/database/src
-RUN chown -R nextjs:nextjs ./packages/database/src && chmod -R 755 ./packages/database/src
-
-COPY --from=installer /app/packages/database/node_modules ./packages/database/node_modules
-RUN chown -R nextjs:nextjs ./packages/database/node_modules && chmod -R 755 ./packages/database/node_modules
-
-COPY --from=installer /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
-RUN chown -R nextjs:nextjs ./packages/database/node_modules/@formbricks/logger/dist && chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
+COPY --from=installer /app/packages/database/dist ./packages/database/dist
+RUN chown -R nextjs:nextjs ./packages/database/dist && chmod -R 755 ./packages/database/dist
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
@@ -142,12 +121,14 @@ RUN chmod -R 755 ./node_modules/@noble/hashes
COPY --from=installer /app/node_modules/zod ./node_modules/zod
RUN chmod -R 755 ./node_modules/zod
-RUN npm install --ignore-scripts -g tsx typescript pino-pretty
RUN npm install -g prisma
+# Create a startup script to handle the conditional logic
+COPY --from=installer /app/apps/web/scripts/docker/next-start.sh /home/nextjs/start.sh
+RUN chown nextjs:nextjs /home/nextjs/start.sh && chmod +x /home/nextjs/start.sh
+
EXPOSE 3000
-ENV HOSTNAME "0.0.0.0"
-ENV NODE_ENV="production"
+ENV HOSTNAME="0.0.0.0"
USER nextjs
# Prepare volume for uploads
@@ -158,12 +139,4 @@ VOLUME /home/nextjs/apps/web/uploads/
RUN mkdir -p /home/nextjs/apps/web/saml-connection
VOLUME /home/nextjs/apps/web/saml-connection
-CMD if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then \
- echo "Starting cron jobs..."; \
- supercronic -quiet /app/docker/cronjobs & \
- else \
- echo "Docker cron jobs are disabled via DOCKER_CRON_ENABLED=0"; \
- fi; \
- (cd packages/database && npm run db:migrate:deploy) && \
- (cd packages/database && npm run db:create-saml-database:deploy) && \
- exec node apps/web/server.js
\ No newline at end of file
+CMD ["/home/nextjs/start.sh"]
\ No newline at end of file
diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.test.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.test.tsx
index 40ab57335f..fb33d1991c 100644
--- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.test.tsx
+++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.test.tsx
@@ -94,6 +94,7 @@ describe("LandingSidebar component", () => {
organizationId: "o1",
redirect: true,
callbackUrl: "/auth/login",
+ clearEnvironmentId: true,
});
});
});
diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.tsx
index ce5e8b7b4a..f50e589875 100644
--- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.tsx
+++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar.tsx
@@ -130,6 +130,7 @@ export const LandingSidebar = ({
organizationId: organization.id,
redirect: true,
callbackUrl: "/auth/login",
+ clearEnvironmentId: true,
});
}}
icon={
+ {t("auth.forgot-password.reset_password_description")}
+
/ {planPeriod === "monthly" ? "Month" : "Year"}
@@ -171,16 +181,9 @@ export const PricingCard = ({
{t("environments.settings.billing.manage_subscription")}
)}
-
- {organization.billing.plan !== plan.id && plan.id === projectFeatureKeys.ENTERPRISE && (
-
{t("common.projects")}
@@ -282,7 +276,7 @@ export const PricingTable = ({