Compare commits

..

1 Commits

Author SHA1 Message Date
Matti Nannt
6c34c316d0 docs: remove non-official self-hosting options from README.md 2026-04-01 14:16:47 +02:00
20 changed files with 3 additions and 263 deletions

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

@@ -4,7 +4,6 @@ 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";
@@ -24,11 +23,9 @@ 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,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
access: [
{
type: "organization",
@@ -42,20 +39,11 @@ export const getResponsesDownloadUrlAction = authenticatedActionClient
],
});
const result = await getResponseDownloadFile(
return 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

@@ -4,7 +4,6 @@ import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { ZIntegrationInput } from "@formbricks/types/integration";
import { createOrUpdateIntegration, deleteIntegration } from "@/lib/integration/service";
import { capturePostHogEvent } from "@/lib/posthog";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import {
@@ -46,12 +45,6 @@ export const createOrUpdateIntegrationAction = authenticatedActionClient
const result = await createOrUpdateIntegration(parsedInput.environmentId, parsedInput.integrationData);
ctx.auditLoggingCtx.integrationId = result.id;
ctx.auditLoggingCtx.newObject = result;
capturePostHogEvent(ctx.user.id, "integration_connected", {
integration_type: parsedInput.integrationData.type,
organization_id: organizationId,
});
return result;
})
);

View File

@@ -1,7 +1,6 @@
import { PipelineTriggers, Webhook } from "@prisma/client";
import { headers } from "next/headers";
import { v7 as uuidv7 } from "uuid";
import { createCacheKey } from "@formbricks/cache";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { ResourceNotFoundError } from "@formbricks/types/errors";
@@ -9,12 +8,10 @@ import { sendTelemetryEvents } from "@/app/api/(internal)/pipeline/lib/telemetry
import { ZPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { cache } from "@/lib/cache";
import { CRON_SECRET } from "@/lib/constants";
import { generateStandardWebhookSignature } from "@/lib/crypto";
import { getIntegrations } from "@/lib/integration/service";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { capturePostHogEvent } from "@/lib/posthog";
import { getResponseCountBySurveyId } from "@/lib/response/service";
import { getSurvey, updateSurvey } from "@/lib/survey/service";
import { convertDatesInObject } from "@/lib/time";
@@ -302,25 +299,6 @@ export const POST = async (request: Request) => {
logger.error({ error, responseId: response.id }, "Failed to record response meter event");
});
// Sampled PostHog tracking: first response + every 100th
const responseCount = await cache.withCache(
() => getResponseCountBySurveyId(surveyId),
createCacheKey.response.countBySurveyId(surveyId),
60 * 1000
);
if (responseCount === 1 || responseCount % 100 === 0) {
capturePostHogEvent(organization.id, "survey_response_received", {
survey_id: surveyId,
survey_type: survey.type,
organization_id: organization.id,
environment_id: environmentId,
response_count: responseCount,
is_first_response: responseCount === 1,
milestone: responseCount === 1 ? "first" : String(responseCount),
});
}
// Send telemetry events
await sendTelemetryEvents();
}

View File

@@ -7,7 +7,6 @@ import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { gethasNoOrganizations } from "@/lib/instance/service";
import { createMembership } from "@/lib/membership/service";
import { createOrganization } from "@/lib/organization/service";
import { capturePostHogEvent } from "@/lib/posthog";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import { ensureCloudStripeSetupForOrganization } from "@/modules/ee/billing/lib/organization-billing";
@@ -50,12 +49,6 @@ export const createOrganizationAction = authenticatedActionClient
ctx.auditLoggingCtx.organizationId = newOrganization.id;
ctx.auditLoggingCtx.newObject = newOrganization;
capturePostHogEvent(ctx.user.id, "organization_created", {
organization_id: newOrganization.id,
is_first_org: hasNoOrganizations,
user_id: ctx.user.id,
});
return newOrganization;
})
);

View File

@@ -1,27 +0,0 @@
import "server-only";
import { logger } from "@formbricks/logger";
import { posthogServerClient } from "./server";
type PostHogEventProperties = Record<string, string | number | boolean | null | undefined>;
export function capturePostHogEvent(
distinctId: string,
eventName: string,
properties?: PostHogEventProperties
): void {
if (!posthogServerClient) return;
try {
posthogServerClient.capture({
distinctId,
event: eventName,
properties: {
...properties,
$lib: "posthog-node",
source: "server",
},
});
} catch (error) {
logger.warn({ error, eventName }, "Failed to capture PostHog event");
}
}

View File

@@ -1 +0,0 @@
export { capturePostHogEvent } from "./capture";

View File

@@ -1,37 +0,0 @@
import "server-only";
import { PostHog } from "posthog-node";
import { logger } from "@formbricks/logger";
import { POSTHOG_KEY } from "@/lib/constants";
const POSTHOG_HOST = "https://eu.i.posthog.com";
const globalForPostHog = globalThis as unknown as {
posthogServerClient: PostHog | undefined;
};
function createPostHogClient(): PostHog | null {
if (!POSTHOG_KEY) return null;
return new PostHog(POSTHOG_KEY, {
host: POSTHOG_HOST,
flushAt: 1,
flushInterval: 0,
});
}
export const posthogServerClient: PostHog | null =
globalForPostHog.posthogServerClient ?? createPostHogClient();
if (process.env.NODE_ENV !== "production" && posthogServerClient) {
globalForPostHog.posthogServerClient = posthogServerClient;
}
if (process.env.NEXT_RUNTIME === "nodejs" && posthogServerClient) {
const shutdownPostHog = () => {
posthogServerClient?.shutdown().catch((err) => {
logger.error(err, "Error shutting down PostHog server client");
});
};
process.on("SIGTERM", shutdownPostHog);
process.on("SIGINT", shutdownPostHog);
}

View File

@@ -10,12 +10,10 @@ import {
EMAIL_VERIFICATION_DISABLED,
ENCRYPTION_KEY,
ENTERPRISE_LICENSE_KEY,
POSTHOG_KEY,
SESSION_MAX_AGE,
} from "@/lib/constants";
import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto";
import { verifyToken } from "@/lib/jwt";
import { capturePostHogEvent } from "@/lib/posthog";
import { updateUser, updateUserLastLoginAt } from "@/modules/auth/lib/user";
import {
logAuthAttempt,
@@ -337,25 +335,6 @@ export const authOptions: NextAuthOptions = {
"";
const userEmail = user.email ?? "";
const userId = user.id as string;
// Capture sign-in event for PostHog (query BEFORE updating lastLoginAt)
const captureSignIn = async (provider: string) => {
if (!POSTHOG_KEY) return;
const [membershipCount, userData] = await Promise.all([
prisma.membership.count({ where: { userId } }),
prisma.user.findUnique({ where: { id: userId }, select: { lastLoginAt: true } }),
]);
const isFirstLoginToday =
!userData?.lastLoginAt || userData.lastLoginAt.toDateString() !== new Date().toDateString();
capturePostHogEvent(userId, "user_signed_in", {
auth_provider: provider,
organization_count: membershipCount,
is_first_login_today: isFirstLoginToday,
});
};
if (account?.provider === "credentials" || account?.provider === "token") {
// check if user's email is verified or not
@@ -363,7 +342,6 @@ export const authOptions: NextAuthOptions = {
logger.error("Email Verification is Pending");
throw new Error("Email Verification is Pending");
}
await captureSignIn(account.provider);
await updateUserLastLoginAt(userEmail);
return true;
}
@@ -375,12 +353,10 @@ export const authOptions: NextAuthOptions = {
});
if (result) {
await captureSignIn(account.provider);
await updateUserLastLoginAt(userEmail);
}
return result;
}
await captureSignIn(account?.provider ?? "unknown");
await updateUserLastLoginAt(userEmail);
return true;
},

View File

@@ -9,7 +9,6 @@ import { IS_FORMBRICKS_CLOUD, IS_TURNSTILE_CONFIGURED, TURNSTILE_SECRET_KEY } fr
import { verifyInviteToken } from "@/lib/jwt";
import { createMembership } from "@/lib/membership/service";
import { createOrganization } from "@/lib/organization/service";
import { capturePostHogEvent } from "@/lib/posthog";
import { actionClient } from "@/lib/utils/action-client";
import { ActionClientCtx } from "@/lib/utils/action-client/types/context";
import { createUser, updateUser } from "@/modules/auth/lib/user";
@@ -212,13 +211,6 @@ export const createUserAction = actionClient.inputSchema(ZCreateUserAction).acti
subscribeToSecurityUpdates: parsedInput.subscribeToSecurityUpdates,
subscribeToProductUpdates: parsedInput.subscribeToProductUpdates,
});
capturePostHogEvent(user.id, "user_signed_up", {
auth_provider: "credentials",
email_domain: user.email.split("@")[1],
signup_source: parsedInput.inviteToken ? "invite" : "direct",
invite_organization_id: ctx.auditLoggingCtx.organizationId ?? null,
});
}
if (user) {

View File

@@ -6,7 +6,6 @@ import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/typ
import { ZCloudBillingInterval } from "@formbricks/types/organizations";
import { WEBAPP_URL } from "@/lib/constants";
import { getOrganization } from "@/lib/organization/service";
import { capturePostHogEvent } from "@/lib/posthog";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
@@ -251,13 +250,6 @@ export const startProTrialAction = authenticatedActionClient
await createProTrialSubscription(parsedInput.organizationId, customerId);
await reconcileCloudStripeSubscriptionsForOrganization(parsedInput.organizationId);
await syncOrganizationBillingFromStripe(parsedInput.organizationId);
capturePostHogEvent(ctx.user.id, "free_trial_started", {
plan: "pro",
organization_id: parsedInput.organizationId,
trial_duration_days: 14,
});
return { success: true };
});

View File

@@ -8,7 +8,6 @@ import { DEFAULT_TEAM_ID, SKIP_INVITE_FOR_SSO } from "@/lib/constants";
import { getIsFreshInstance } from "@/lib/instance/service";
import { verifyInviteToken } from "@/lib/jwt";
import { createMembership } from "@/lib/membership/service";
import { capturePostHogEvent } from "@/lib/posthog";
import { findMatchingLocale } from "@/lib/utils/locale";
import { redactPII } from "@/lib/utils/logger-helpers";
import { createBrevoCustomer } from "@/modules/auth/lib/brevo";
@@ -361,14 +360,6 @@ export const handleSsoCallback = async ({
// send new user to brevo
createBrevoCustomer({ id: userProfile.id, email: userProfile.email });
capturePostHogEvent(userProfile.id, "user_signed_up", {
auth_provider: provider,
email_domain: userProfile.email.split("@")[1],
signup_source: callbackUrl?.includes("token=") ? "invite" : "direct",
invite_organization_id: organization?.id ?? null,
});
await syncSsoAccount(userProfile.id, account);
if (isMultiOrgEnabled) {

View File

@@ -10,7 +10,6 @@ import { INVITE_DISABLED, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { createInviteToken } from "@/lib/jwt";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import { capturePostHogEvent } from "@/lib/posthog";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getOrganizationIdFromInviteId } from "@/lib/utils/helper";
@@ -328,11 +327,6 @@ export const inviteUserAction = authenticatedActionClient.inputSchema(ZInviteUse
await sendInviteMemberEmail(inviteId, parsedInput.email, ctx.user.name ?? "", parsedInput.name ?? "");
}
capturePostHogEvent(ctx.user.id, "team_member_invited", {
organization_id: parsedInput.organizationId,
invitee_role: parsedInput.role,
});
return inviteId;
})
);

View File

@@ -3,7 +3,6 @@
import { z } from "zod";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ZSurveyCreateInput } from "@formbricks/types/surveys/types";
import { capturePostHogEvent } from "@/lib/posthog";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
@@ -16,7 +15,6 @@ import { getOrganizationBilling } from "@/modules/survey/lib/survey";
const ZCreateSurveyAction = z.object({
environmentId: z.cuid2(),
surveyBody: ZSurveyCreateInput,
createdFrom: z.enum(["blank", "template"]).optional(),
});
/**
@@ -70,15 +68,6 @@ export const createSurveyAction = authenticatedActionClient.inputSchema(ZCreateS
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.surveyId = result.id;
ctx.auditLoggingCtx.newObject = result;
capturePostHogEvent(ctx.user.id, "survey_created", {
survey_id: result.id,
survey_type: result.type,
organization_id: organizationId,
question_count: result.questions?.length ?? 0,
created_from: parsedInput.createdFrom ?? "template",
});
return result;
})
);

View File

@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
import { ZProjectConfigChannel, ZProjectConfigIndustry } from "@formbricks/types/project";
import { TSurveyCreateInput, TSurveyType } from "@formbricks/types/surveys/types";
import { TTemplate, TTemplateFilter, ZTemplateRole } from "@formbricks/types/templates";
import { customSurveyTemplate, templates } from "@/app/lib/templates";
import { templates } from "@/app/lib/templates";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { createSurveyAction } from "./actions";
import { StartFromScratchTemplate } from "./components/start-from-scratch-template";
@@ -58,11 +58,9 @@ export const TemplateList = ({
type: surveyType,
createdBy: userId,
};
const isBlank = activeTemplate.name === customSurveyTemplate(t).name;
const createSurveyResponse = await createSurveyAction({
environmentId: environmentId,
surveyBody: augmentedTemplate,
createdFrom: isBlank ? "blank" : "template",
});
if (createSurveyResponse?.data) {

View File

@@ -6,7 +6,6 @@ import { ZActionClassInput } from "@formbricks/types/action-classes";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TSurvey, ZSurvey } from "@formbricks/types/surveys/types";
import { UNSPLASH_ACCESS_KEY, UNSPLASH_ALLOWED_DOMAINS } from "@/lib/constants";
import { capturePostHogEvent } from "@/lib/posthog";
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import {
@@ -146,17 +145,6 @@ export const updateSurveyAction = authenticatedActionClient.inputSchema(ZSurvey)
ctx.auditLoggingCtx.oldObject = oldObject;
ctx.auditLoggingCtx.newObject = result;
if (oldObject?.status !== "inProgress" && result.status === "inProgress") {
capturePostHogEvent(ctx.user.id, "survey_published", {
survey_id: result.id,
survey_type: result.type,
question_count: result.questions?.length ?? 0,
organization_id: organizationId,
has_targeting: result.segment ? !result.segment.isPrivate : false,
language_count: result.languages?.length ?? 0,
});
}
revalidatePath(`/environments/${result.environmentId}/surveys/${result.id}`);
return result;

View File

@@ -33,7 +33,6 @@ const nextConfig = {
"pino",
"pino-pretty",
"pino-opentelemetry-transport",
"posthog-node",
],
outputFileTracingIncludes: {
"/api/auth/**/*": ["../../node_modules/jose/**/*"],

View File

@@ -104,7 +104,6 @@
"otplib": "12.0.1",
"papaparse": "5.5.3",
"posthog-js": "1.360.0",
"posthog-node": "5.28.9",
"prismjs": "1.30.0",
"qr-code-styling": "1.9.2",
"qrcode": "1.5.4",

View File

@@ -35,11 +35,6 @@ export const createCacheKey = {
fetch_lock: (organizationId: string): CacheKey => makeCacheKey("license", organizationId, "fetch_lock"),
},
// Response-related keys
response: {
countBySurveyId: (surveyId: string): CacheKey => makeCacheKey("response", surveyId, "count"),
},
// Rate limiting and security
rateLimit: {
core: (namespace: string, identifier: string, windowStart: number): CacheKey =>

25
pnpm-lock.yaml generated
View File

@@ -369,9 +369,6 @@ importers:
posthog-js:
specifier: 1.360.0
version: 1.360.0
posthog-node:
specifier: 5.28.9
version: 5.28.9(rxjs@7.8.2)
prismjs:
specifier: 1.30.0
version: 1.30.0
@@ -3315,9 +3312,6 @@ packages:
'@posthog/core@1.23.2':
resolution: {integrity: sha512-zTDdda9NuSHrnwSOfFMxX/pyXiycF4jtU1kTr8DL61dHhV+7LF6XF1ndRZZTuaGGbfbb/GJYkEsjEX9SXfNZeQ==}
'@posthog/core@1.24.4':
resolution: {integrity: sha512-S+TolwBHSSJz7WWtgaELQWQqXviSm3uf1e+qorWUts0bZcgPwWzhnmhCUZAhvn0NVpTQHDJ3epv+hHbPLl5dHg==}
'@posthog/types@1.360.0':
resolution: {integrity: sha512-roypbiJ49V3jWlV/lzhXGf0cKLLRj69L4H4ZHW6YsITHlnjQ12cgdPhPS88Bb9nW9xZTVSGWWDjfNGsdgAxsNg==}
@@ -9121,15 +9115,6 @@ packages:
posthog-js@1.360.0:
resolution: {integrity: sha512-jkyO+T97yi6RuiexOaXC7AnEGiC+yIfGU5DIUzI5rqBH6MltmtJw/ve2Oxc4jeua2WDr5sXMzo+SS+acbpueAA==}
posthog-node@5.28.9:
resolution: {integrity: sha512-iZWyAYkIAq5QqcYz4q2nXOX+Ivn04Yh8AuKqfFVw0SvBpfli49bNAjyE97qbRTLr+irrzRUELgGIkDC14NgugA==}
engines: {node: ^20.20.0 || >=22.22.0}
peerDependencies:
rxjs: ^7.0.0
peerDependenciesMeta:
rxjs:
optional: true
powershell-utils@0.1.0:
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
engines: {node: '>=20'}
@@ -14489,10 +14474,6 @@ snapshots:
dependencies:
cross-spawn: 7.0.6
'@posthog/core@1.24.4':
dependencies:
cross-spawn: 7.0.6
'@posthog/types@1.360.0': {}
'@preact/preset-vite@2.10.3(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.59.0)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
@@ -21022,12 +21003,6 @@ snapshots:
query-selector-shadow-dom: 1.0.1
web-vitals: 5.1.0
posthog-node@5.28.9(rxjs@7.8.2):
dependencies:
'@posthog/core': 1.24.4
optionalDependencies:
rxjs: 7.8.2
powershell-utils@0.1.0: {}
preact-render-to-string@5.2.6(preact@10.28.2):