Compare commits

...

25 Commits

Author SHA1 Message Date
Johannes
f356e86729 vibed settings unification 2026-04-12 20:48:07 +02:00
Manuel Delgado
1a02f91afd fix(api): return 409 Conflict instead of 500 when creating user with duplicate email (#7675)
Co-authored-by: Tiago Farto <tiago@formbricks.com>
2026-04-10 14:28:17 +00:00
Tiago
cc22ccb22d chore: Harden SSO account linking for existing email-based accounts (#7702) 2026-04-10 14:19:21 +00:00
Tiago
12763f0ef6 fix: Dutch translations for link survey footer (Privacy Policy, Imprint, Report Survey) (#7707) 2026-04-10 13:42:15 +00:00
Dhruwang Jariwala
d39e3ee638 feat: offline support for link surveys (#7694)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2026-04-10 11:27:48 +00:00
dingdyan
d85242a86b fix: handle internal server error toast behavior in create organization (#7662)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-04-10 11:13:10 +00:00
Bhagya Amarasinghe
ef53065abc feat: support GKE Envoy ingress split with numeric ports and service annotations (#7704) 2026-04-10 09:22:19 +00:00
Dhruwang Jariwala
805c1c6874 fix: (duplicate) server error toast handling (#7701) 2026-04-10 09:22:16 +00:00
Niels Kaspers
01687e8907 fix: add TERMS_URL support to survey link footers (#7670) 2026-04-10 09:21:11 +00:00
Johannes
31d455002d feat: unifiy nav auth behaviour (#7635)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Johannes <jobenjada@users.noreply.github.com>
2026-04-09 14:26:14 +00:00
Johannes
d96304d86d fix: make navigation more user-friendly (#7599)
Co-authored-by: Tiago Farto <tiago@formbricks.com>
2026-04-09 08:03:24 +00:00
Bhagya Amarasinghe
1064f68435 fix: support OTEL host config for envoy telemetry (#7692) 2026-04-09 07:25:52 +00:00
Anshuman Pandey
3d16e859c6 feat: custom posthog events (#7647) 2026-04-09 05:34:01 +00:00
Salim B
af198c5632 docs: remove spurious left-overs (#7690) 2026-04-08 16:11:30 +00:00
Bhagya Amarasinghe
a43ed2b25c feat: add envoy gateway helm bundle (#7686) 2026-04-08 07:34:47 +00:00
Tiago
87bcad2b20 feat: Supporting different AI providers within Formbricks (#7611)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-04-06 05:45:12 +00:00
Anshuman Pandey
b5eaa4c7fd fix: merge epic/improve-telemetry into main (#7666) 2026-04-03 10:12:51 +00:00
Tiago
995c03bc01 chore: Revoke all active sessions after password reset (#7628) 2026-04-03 06:10:28 +00:00
Johannes
b4395a48c5 fix: multi-lang toggle covering arabic text (#7657)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-04-02 13:09:16 +00:00
Johannes
461e3893fe fix: 7549 multilang button overflow (#7656)
Co-authored-by: Niels Kaspers <kaspersniels@gmail.com>
2026-04-02 12:53:57 +00:00
Tiago
735a9f84ec fix: harden api error reporting for v2/v1 Sentry observability (#7633) 2026-04-02 12:08:44 +00:00
Dhruwang Jariwala
8cb8d734cf fix: prevent language switch from breaking survey orientation and resetting language on auto-save (#7654)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 12:08:12 +00:00
Anshuman Pandey
44d5530b48 fix: adds formbricks instance on window (#7630) 2026-04-02 07:26:48 +00:00
Matti Nannt
a314eb391e chore: add Codex environment config (#7589) 2026-04-02 07:24:02 +00:00
Matti Nannt
6c34c316d0 docs: remove non-official self-hosting options from README.md 2026-04-01 14:16:47 +02:00
403 changed files with 14648 additions and 3439 deletions

View File

@@ -0,0 +1,9 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "formbricks"
[setup]
script = '''
pnpm install
pnpm dev:setup
'''

View File

@@ -138,6 +138,31 @@ AZUREAD_CLIENT_ID=
AZUREAD_CLIENT_SECRET=
AZUREAD_TENANT_ID=
# Configure Formbricks AI at the instance level
# Set the provider used for AI features on this instance.
# Accepted values for AI_PROVIDER: aws, gcp, azure
# Set AI_MODEL to the provider-specific model or deployment name and configure the matching credentials below.
# AI_PROVIDER=gcp
# AI_MODEL=gemini-2.5-flash
# Google Vertex AI credentials
# AI_GCP_PROJECT=
# AI_GCP_LOCATION=
# AI_GCP_CREDENTIALS_JSON=
# AI_GCP_APPLICATION_CREDENTIALS=
# Amazon Bedrock credentials
# AI_AWS_REGION=
# AI_AWS_ACCESS_KEY_ID=
# AI_AWS_SECRET_ACCESS_KEY=
# AI_AWS_SESSION_TOKEN=
# Azure AI / Microsoft Foundry credentials
# AI_AZURE_BASE_URL=
# AI_AZURE_RESOURCE_NAME=
# AI_AZURE_API_KEY=
# AI_AZURE_API_VERSION=v1
# OpenID Connect (OIDC) configuration
# OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET=
@@ -191,6 +216,9 @@ ENTERPRISE_LICENSE_KEY=
# Ignore Rate Limiting across the Formbricks app
# RATE_LIMITING_DISABLED=1
# Disable telemetry reporting (usage stats sent to Formbricks). Ignored when an EE license is active.
# TELEMETRY_DISABLED=1
# Allow webhook URLs to point to internal/private network addresses (e.g. localhost, 192.168.x.x)
# WARNING: Only enable this if you understand the SSRF risks. Useful for self-hosted instances
# that need to send webhooks to internal services.

View File

@@ -127,34 +127,10 @@ Formbricks has a hosted cloud offering with a generous free plan to get you up a
Formbricks is available Open-Source under AGPLv3 license. You can host Formbricks on your own servers using Docker without a subscription.
If you opt for self-hosting Formbricks, here are a few options to consider:
#### Docker
To get started with self-hosting with Docker, take a look at our [self-hosting docs](https://formbricks.com/docs/self-hosting/deployment).
#### Community-managed One Click Hosting
##### Railway
You can deploy Formbricks on [Railway](https://railway.app) using the button below.
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/PPDzCd)
##### RepoCloud
Or you can also deploy Formbricks on [RepoCloud](https://repocloud.io) using the button below.
[![Deploy on RepoCloud](https://d16t0pc4846x52.cloudfront.net/deploy.png)](https://repocloud.io/details/?app_id=254)
##### Zeabur
Or you can also deploy Formbricks on [Zeabur](https://zeabur.com) using the button below.
[![Deploy to Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/G4TUJL)
<a id="development"></a>
## 👨‍💻 Development
### Prerequisites
@@ -248,14 +224,3 @@ We currently do not offer Formbricks white-labeled. That means that we don't sel
The Enterprise Edition allows us to fund the development of Formbricks sustainably. It guarantees that the free and open-source surveying infrastructure we're building will be around for decades to come.
<a id="readme-de"></a>
## Deutsch
Formbricks ist eine freie, quelloffene und datenschutzorientierte Plattform für Surveys und Experience Management. Mit In-App-, Website-, Link- und E-Mail-Umfragen sammelt ihr Feedback entlang der gesamten User Journey.
- Website & Cloud: [formbricks.com](https://formbricks.com/) und [Cloud starten](https://app.formbricks.com/auth/signup)
- Self-Hosting: [Deployment-Dokumentation](https://formbricks.com/docs/self-hosting/deployment)
- Beitrag & Community: [Beitragen](https://formbricks.com/docs/developer-docs/contributing/get-started), [GitHub Discussions](https://github.com/formbricks/formbricks/discussions) und [Issues](https://github.com/formbricks/formbricks/issues)
- Sicherheit & Lizenz: [`SECURITY.md`](./SECURITY.md) und [AGPLv3](https://github.com/formbricks/formbricks/blob/main/LICENSE)
<p align="right"><a href="#top">🔼 Back to top</a></p>

View File

@@ -26,7 +26,8 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
const { isMember } = getAccessFlags(membership?.role);
const { isBilling } = getAccessFlags(membership?.role);
const isMembershipPending = membership?.role === undefined;
return (
<div className="flex min-h-full min-w-full flex-row">
@@ -44,7 +45,8 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
isLicenseActive={false}
isOwnerOrManager={false}
isAccessControlAllowed={false}
isMember={isMember}
isBilling={isBilling}
isMembershipPending={isMembershipPending}
environments={[]}
/>
</div>

View File

@@ -0,0 +1,24 @@
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getEnvironmentLayoutData } from "@/modules/environments/lib/utils";
const MainNavLayout = async (props: {
params: Promise<{ environmentId: string }>;
children: React.ReactNode;
}) => {
const params = await props.params;
const { children } = props;
const session = await getServerSession(authOptions);
if (!session?.user) {
return redirect("/auth/login");
}
const layoutData = await getEnvironmentLayoutData(params.environmentId, session.user.id);
return <EnvironmentLayout layoutData={layoutData}>{children}</EnvironmentLayout>;
};
export default MainNavLayout;

View File

@@ -29,6 +29,7 @@ import { ResponseTableCell } from "@/app/(app)/environments/[environmentId]/surv
import { generateResponseTableColumns } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTableColumns";
import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
import { downloadResponsesFile } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/utils";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { deleteResponseAction } from "@/modules/analysis/components/SingleResponseCard/actions";
import { Button } from "@/modules/ui/components/button";
import {
@@ -201,7 +202,13 @@ export const ResponseTable = ({
};
const deleteResponse = async (responseId: string, params?: { decrementQuotas?: boolean }) => {
await deleteResponseAction({ responseId, decrementQuotas: params?.decrementQuotas ?? false });
const result = await deleteResponseAction({
responseId,
decrementQuotas: params?.decrementQuotas ?? false,
});
if (result?.serverError) {
throw new Error(getFormattedErrorMessage(result));
}
};
// Handle downloading selected responses

View File

@@ -163,6 +163,7 @@ export const PersonalLinksTab = ({
<UpgradePrompt
title={t("environments.surveys.share.personal_links.upgrade_prompt_title")}
description={t("environments.surveys.share.personal_links.upgrade_prompt_description")}
feature="personal_links"
buttons={[
{
text: isFormbricksCloud ? t("common.upgrade_plan") : t("common.request_trial_license"),

View File

@@ -4,6 +4,7 @@ import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
import { capturePostHogEvent } from "@/lib/posthog";
import { getResponseDownloadFile, getResponseFilteringValues } from "@/lib/response/service";
import { getSurvey } from "@/lib/survey/service";
import { getTagsByEnvironmentId } from "@/lib/tag/service";
@@ -23,9 +24,11 @@ const ZGetResponsesDownloadUrlAction = z.object({
export const getResponsesDownloadUrlAction = authenticatedActionClient
.inputSchema(ZGetResponsesDownloadUrlAction)
.action(async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
organizationId,
access: [
{
type: "organization",
@@ -39,11 +42,20 @@ export const getResponsesDownloadUrlAction = authenticatedActionClient
],
});
return await getResponseDownloadFile(
const result = await getResponseDownloadFile(
parsedInput.surveyId,
parsedInput.format,
parsedInput.filterCriteria
);
capturePostHogEvent(ctx.user.id, "responses_exported", {
survey_id: parsedInput.surveyId,
format: parsedInput.format,
filter_applied: Object.keys(parsedInput.filterCriteria ?? {}).length > 0,
organization_id: organizationId,
});
return result;
});
const ZGetSurveyFilterDataAction = z.object({

View File

@@ -0,0 +1,28 @@
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { SettingsSidebar } from "@/modules/settings/components/settings-sidebar";
const SettingsLayout = async (props: {
params: Promise<{ environmentId: string }>;
children: React.ReactNode;
}) => {
const params = await props.params;
const { children } = props;
const { project, organization, currentUserMembership } = await getEnvironmentAuth(params.environmentId);
return (
<div className="flex h-screen min-h-screen overflow-hidden">
<SettingsSidebar
environmentId={params.environmentId}
projectName={project.name}
organizationName={organization.name}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
membershipRole={currentUserMembership?.role}
/>
<div className="flex flex-1 flex-col overflow-y-auto bg-slate-50">{children}</div>
</div>
);
};
export default SettingsLayout;

View File

@@ -0,0 +1,139 @@
import { getServerSession } from "next-auth";
import { prisma } from "@formbricks/database";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TUserNotificationSettings } from "@formbricks/types/user";
import { EditAlerts } from "@/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts";
import { IntegrationsTip } from "@/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/IntegrationsTip";
import type { Membership } from "@/app/(app)/environments/[environmentId]/settings/(account)/notifications/types";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { getUser } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
const setCompleteNotificationSettings = (
notificationSettings: TUserNotificationSettings,
memberships: Membership[]
): TUserNotificationSettings => {
const newNotificationSettings: TUserNotificationSettings = {
alert: {} as Record<string, boolean>,
unsubscribedOrganizationIds: notificationSettings.unsubscribedOrganizationIds || [],
};
for (const membership of memberships) {
for (const project of membership.organization.projects) {
for (const environment of project.environments) {
for (const survey of environment.surveys) {
newNotificationSettings.alert[survey.id] =
(notificationSettings as unknown as Record<string, Record<string, boolean>>)[survey.id]
?.responseFinished ||
(notificationSettings.alert && notificationSettings.alert[survey.id]) ||
false;
}
}
}
}
return newNotificationSettings;
};
const getMemberships = async (userId: string): Promise<Membership[]> => {
const memberships = await prisma.membership.findMany({
where: {
userId,
role: { not: "billing" },
OR: [
{ role: { in: ["owner", "manager"] } },
{
organization: {
projects: {
some: {
projectTeams: { some: { team: { teamUsers: { some: { userId } } } } },
},
},
},
},
],
},
select: {
organization: {
select: {
id: true,
name: true,
projects: {
where: {
OR: [
{
organization: {
memberships: { some: { userId, role: { in: ["owner", "manager"] } } },
},
},
{ projectTeams: { some: { team: { teamUsers: { some: { userId } } } } } },
],
},
select: {
id: true,
name: true,
environments: {
where: { type: "production" },
select: {
id: true,
surveys: { select: { id: true, name: true } },
},
},
},
},
},
},
},
});
return memberships;
};
const Page = async (props: {
params: Promise<{ environmentId: string }>;
searchParams: Promise<Record<string, string>>;
}) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslate();
const session = await getServerSession(authOptions);
if (!session) {
throw new AuthenticationError(t("common.not_authenticated"));
}
const autoDisableNotificationType = searchParams["type"];
const autoDisableNotificationElementId = searchParams["elementId"];
const [user, memberships] = await Promise.all([getUser(session.user.id), getMemberships(session.user.id)]);
if (!user) {
throw new AuthenticationError(t("common.not_authenticated"));
}
if (!memberships) {
throw new ResourceNotFoundError(t("common.membership"), null);
}
if (user?.notificationSettings) {
user.notificationSettings = setCompleteNotificationSettings(user.notificationSettings, memberships);
}
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.account_settings")} />
<SettingsCard
title={t("environments.settings.notifications.email_alerts_surveys")}
description={t(
"environments.settings.notifications.set_up_an_alert_to_get_an_email_on_new_responses"
)}>
<EditAlerts
memberships={memberships}
user={user}
environmentId={params.environmentId}
autoDisableNotificationType={autoDisableNotificationType}
autoDisableNotificationElementId={autoDisableNotificationElementId}
/>
</SettingsCard>
<IntegrationsTip environmentId={params.environmentId} />
</PageContentWrapper>
);
};
export default Page;

View File

@@ -0,0 +1,99 @@
import { AuthenticationError } from "@formbricks/types/errors";
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
import { DeleteAccount } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DeleteAccount";
import { EditProfileDetailsForm } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD, PASSWORD_RESET_DISABLED } from "@/lib/constants";
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const isTwoFactorAuthEnabled = await getIsTwoFactorAuthEnabled();
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
const params = await props.params;
const t = await getTranslate();
const { environmentId } = params;
const { session } = await getEnvironmentAuth(params.environmentId);
const organizationsWithSingleOwner = await getOrganizationsWhereUserIsSingleOwner(session.user.id);
const user = session?.user ? await getUser(session.user.id) : null;
if (!user) {
throw new AuthenticationError(t("common.not_authenticated"));
}
const isPasswordResetEnabled = !PASSWORD_RESET_DISABLED && user.identityProvider === "email";
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.account_settings")} />
{user && (
<div>
<SettingsCard
title={t("environments.settings.profile.personal_information")}
description={t("environments.settings.profile.update_personal_info")}>
<EditProfileDetailsForm
user={user}
emailVerificationDisabled={EMAIL_VERIFICATION_DISABLED}
isPasswordResetEnabled={isPasswordResetEnabled}
/>
</SettingsCard>
{user.identityProvider === "email" && (
<SettingsCard
title={t("common.security")}
description={t("environments.settings.profile.security_description")}>
{!isTwoFactorAuthEnabled && !user.twoFactorEnabled ? (
<UpgradePrompt
title={t("environments.settings.profile.unlock_two_factor_authentication")}
description={t("environments.settings.profile.two_factor_authentication_description")}
buttons={[
{
text: IS_FORMBRICKS_CLOUD
? t("common.upgrade_plan")
: t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${environmentId}/settings/organization/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/environments/${environmentId}/settings/organization/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
) : (
<AccountSecurity user={user} />
)}
</SettingsCard>
)}
<SettingsCard
title={t("environments.settings.profile.delete_account")}
description={t("environments.settings.profile.confirm_delete_account")}>
<DeleteAccount
session={session}
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
user={user}
organizationsWithSingleOwner={organizationsWithSingleOwner}
isMultiOrgEnabled={isMultiOrgEnabled}
/>
</SettingsCard>
<IdBadge id={user.id} label={t("common.profile_id")} variant="column" />
</div>
)}
</PageContentWrapper>
);
};
export default Page;

Some files were not shown because too many files have changed in this diff Show More