14 KiB
Enterprise Feature Access: Status Quo Analysis
Executive Summary
Formbricks currently uses two completely different mechanisms to gate enterprise features depending on deployment type:
| Deployment | Gating Mechanism | Activation | Feature Control |
|---|---|---|---|
Cloud (IS_FORMBRICKS_CLOUD=1) |
Billing Plan (organization.billing.plan) |
Stripe subscription | Plan-based (FREE/STARTUP/CUSTOM) |
| On-Premise | License Key (ENTERPRISE_LICENSE_KEY) |
License API validation | License feature flags |
This dual approach creates significant complexity, code duplication, and inconsistent behavior across the codebase.
1. Core Architecture
1.1 Cloud (Formbricks Cloud)
Source of Truth: organization.billing.plan
// packages/database/zod/organizations.ts
plan: z.enum(["free", "startup", "scale", "enterprise"]).default("free")
Plans and Limits:
FREE: 3 projects, 1,500 responses/month, 2,000 MIUSTARTUP: 3 projects, 5,000 responses/month, 7,500 MIUCUSTOM: Unlimited (negotiated limits)
Activation: Stripe webhook updates organization.billing on checkout/subscription events.
1.2 On-Premise (Self-Hosted)
Source of Truth: ENTERPRISE_LICENSE_KEY environment variable
License Features Schema:
// apps/web/modules/ee/license-check/types/enterprise-license.ts
{
isMultiOrgEnabled: boolean,
contacts: boolean,
projects: number | null,
whitelabel: boolean,
removeBranding: boolean,
twoFactorAuth: boolean,
sso: boolean,
saml: boolean,
spamProtection: boolean,
ai: boolean,
auditLogs: boolean,
multiLanguageSurveys: boolean,
accessControl: boolean,
quotas: boolean,
}
Activation: License key validated against https://ee.formbricks.com/api/licenses/check (cached for 24h, grace period of 3 days).
2. Feature Gating Patterns
2.1 Pattern A: Dual-Path Check (Most Common)
Features that need both Cloud billing and on-premise license checks:
// apps/web/modules/ee/license-check/lib/utils.ts
const getFeaturePermission = async (billingPlan, featureKey) => {
const license = await getEnterpriseLicense();
if (IS_FORMBRICKS_CLOUD) {
return license.active && billingPlan !== PROJECT_FEATURE_KEYS.FREE;
} else {
return license.active && !!license.features?.[featureKey];
}
};
Used by:
getRemoveBrandingPermission()- Remove brandinggetWhiteLabelPermission()- Whitelabel featuresgetBiggerUploadFileSizePermission()- Large file uploadsgetIsSpamProtectionEnabled()- reCAPTCHA spam protectiongetMultiLanguagePermission()- Multi-language surveysgetAccessControlPermission()- Teams & rolesgetIsQuotasEnabled()- Quota managementgetOrganizationProjectsLimit()- Project limits
2.2 Pattern B: License-Only Check
Features checked only against license (works same for cloud and on-premise):
// apps/web/modules/ee/license-check/lib/utils.ts
const getSpecificFeatureFlag = async (featureKey) => {
const licenseFeatures = await getLicenseFeatures();
if (!licenseFeatures) return false;
return licenseFeatures[featureKey] ?? false;
};
Used by:
getIsMultiOrgEnabled()- Multiple organizationsgetIsContactsEnabled()- Contacts & segmentsgetIsTwoFactorAuthEnabled()- 2FAgetIsSsoEnabled()- SSOgetIsAuditLogsEnabled()- Audit logs
2.3 Pattern C: Cloud-Only (No License Check)
Features available only on Cloud, gated purely by billing plan:
// apps/web/modules/survey/lib/permission.ts
export const getExternalUrlsPermission = async (billingPlan) => {
if (IS_FORMBRICKS_CLOUD) return billingPlan !== PROJECT_FEATURE_KEYS.FREE;
return true; // Always allowed on self-hosted
};
Used by:
- External URLs permission
- Survey follow-ups (Custom plan only)
2.4 Pattern D: On-Premise Only (Disabled on Cloud)
Features explicitly disabled on Cloud:
// apps/web/modules/ee/license-check/lib/utils.ts
export const getIsSamlSsoEnabled = async () => {
if (IS_FORMBRICKS_CLOUD) return false; // Never on Cloud
const licenseFeatures = await getLicenseFeatures();
return licenseFeatures.sso && licenseFeatures.saml;
};
Used by:
- SAML SSO
- Pretty URLs (slug feature)
- Domain/Organization settings page
3. Files Using Enterprise Features
3.1 Core License/Feature Check Files
| File | Purpose |
|---|---|
apps/web/modules/ee/license-check/lib/license.ts |
License fetching & caching |
apps/web/modules/ee/license-check/lib/utils.ts |
Permission check functions |
apps/web/modules/ee/license-check/types/enterprise-license.ts |
Type definitions |
apps/web/lib/constants.ts |
IS_FORMBRICKS_CLOUD, ENTERPRISE_LICENSE_KEY |
3.2 Feature-Specific Implementation Files
Remove Branding
apps/web/modules/ee/whitelabel/remove-branding/actions.tsapps/web/modules/ee/whitelabel/remove-branding/components/branding-settings-card.tsxapps/web/modules/projects/settings/look/page.tsxapps/web/modules/projects/settings/actions.ts
Whitelabel / Email Customization
apps/web/modules/ee/whitelabel/email-customization/actions.tsapps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsxapps/web/app/(app)/environments/[environmentId]/settings/(organization)/domain/page.tsx
Multi-Language Surveys
apps/web/modules/ee/multi-language-surveys/lib/actions.tsapps/web/modules/ee/multi-language-surveys/components/*.tsxapps/web/modules/ee/languages/page.tsx
Contacts & Segments
apps/web/modules/ee/contacts/segments/actions.tsapps/web/modules/ee/contacts/page.tsxapps/web/modules/ee/contacts/api/v1/**/*.tsapps/web/modules/ee/contacts/api/v2/**/*.ts
Teams & Access Control
apps/web/modules/ee/teams/team-list/components/teams-view.tsxapps/web/modules/ee/role-management/actions.tsapps/web/modules/organization/settings/teams/page.tsxapps/web/modules/organization/settings/teams/actions.ts
SSO / SAML
apps/web/modules/ee/sso/lib/sso-handlers.tsapps/web/modules/ee/auth/saml/api/**/*.tsapps/web/modules/ee/auth/saml/lib/*.tsapps/web/modules/auth/lib/authOptions.ts
Two-Factor Authentication
apps/web/modules/ee/two-factor-auth/actions.tsapps/web/modules/ee/two-factor-auth/components/*.tsx
Quotas
apps/web/modules/ee/quotas/actions.tsapps/web/modules/ee/quotas/components/*.tsxapps/web/modules/ee/quotas/lib/*.ts
Audit Logs
apps/web/modules/ee/audit-logs/lib/handler.tsapps/web/modules/ee/audit-logs/lib/service.ts
Billing (Cloud Only)
apps/web/modules/ee/billing/page.tsxapps/web/modules/ee/billing/api/lib/*.tsapps/web/modules/ee/billing/components/*.tsx
3.3 API Routes Using Feature Checks
| Route | Feature Check |
|---|---|
apps/web/app/api/v1/client/[environmentId]/responses/route.ts |
Spam protection |
apps/web/app/api/v2/client/[environmentId]/responses/route.ts |
Spam protection |
apps/web/app/api/v1/client/[environmentId]/environment/lib/environmentState.ts |
Cloud limits |
apps/web/modules/ee/contacts/api/v1/client/[environmentId]/user/route.ts |
Contacts |
apps/web/modules/api/v2/management/responses/lib/response.ts |
Cloud limits |
3.4 UI Pages with Conditional Rendering
| Page | Condition |
|---|---|
apps/web/app/(app)/environments/[environmentId]/settings/(organization)/billing/ |
Cloud only |
apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx |
On-premise only |
apps/web/app/(app)/environments/[environmentId]/settings/(organization)/domain/page.tsx |
On-premise only |
apps/web/app/p/[slug]/page.tsx (Pretty URLs) |
On-premise only |
4. Configuration & Environment Variables
4.1 Key Environment Variables
| Variable | Purpose | Default |
|---|---|---|
IS_FORMBRICKS_CLOUD |
Enables cloud mode | "0" |
ENTERPRISE_LICENSE_KEY |
License key for on-premise | (empty) |
STRIPE_SECRET_KEY |
Stripe API key (Cloud) | (empty) |
AUDIT_LOG_ENABLED |
Enable audit logs | "0" |
SAML_DATABASE_URL |
SAML configuration DB | (empty) |
4.2 Database Schema
// Organization billing stored in JSON column
billing: {
stripeCustomerId: string | null,
plan: "free" | "startup" | "scale" | "enterprise",
period: "monthly" | "yearly",
limits: {
projects: number | null,
monthly: {
responses: number | null,
miu: number | null,
}
},
periodStart: Date | null
}
5. Problems with Current Approach
5.1 Code Duplication
Almost every feature check function has this pattern:
if (IS_FORMBRICKS_CLOUD) {
// Check billing plan
} else {
// Check license feature
}
This is repeated in:
- 8+ permission check functions in
utils.ts - 30+ files that consume these functions
- Multiple API routes and pages
5.2 Inconsistent Feature Gating
| Feature | Cloud Gating | On-Premise Gating |
|---|---|---|
| Remove Branding | plan !== FREE |
license.features.removeBranding |
| Multi-Language | plan === CUSTOM OR license.multiLanguageSurveys |
license.multiLanguageSurveys |
| Follow-ups | plan === CUSTOM |
Always allowed |
| SAML SSO | Never allowed | license.sso && license.saml |
| Teams | plan === CUSTOM OR license.accessControl |
license.accessControl |
5.3 Confusing License Requirement on Cloud
Cloud deployments still require ENTERPRISE_LICENSE_KEY to be set for enterprise features to work:
// utils.ts - getFeaturePermission
if (IS_FORMBRICKS_CLOUD) {
return license.active && billingPlan !== PROJECT_FEATURE_KEYS.FREE;
// ^^^^^^^^^^^^^^ Still checks license!
}
This means Cloud needs both:
- Active billing plan (Stripe subscription)
- Active enterprise license
5.4 Fallback Logic Complexity
const featureFlagFallback = async (billingPlan) => {
const license = await getEnterpriseLicense();
if (IS_FORMBRICKS_CLOUD) return license.active && billingPlan === PROJECT_FEATURE_KEYS.CUSTOM;
else if (!IS_FORMBRICKS_CLOUD) return license.active;
return false;
};
Features have "fallback" behavior for backwards compatibility, adding another layer of complexity.
5.5 Testing Complexity
Tests must mock both:
IS_FORMBRICKS_CLOUDconstantgetEnterpriseLicense()functionorganization.billing.planin some cases
See: apps/web/modules/ee/license-check/lib/utils.test.ts (400+ lines of test mocking)
6. Feature Availability Matrix
| Feature | Free (Cloud) | Startup (Cloud) | Custom (Cloud) | No License (On-Prem) | License (On-Prem) |
|---|---|---|---|---|---|
| Remove Branding | ❌ | ✅ | ✅ | ❌ | ✅* |
| Whitelabel | ❌ | ✅ | ✅ | ❌ | ✅* |
| Multi-Language | ❌ | ❌ | ✅ | ❌ | ✅* |
| Teams & Roles | ❌ | ❌ | ✅ | ❌ | ✅* |
| Contacts | ❌ | ❌ | ❌ | ❌ | ✅* |
| SSO (OIDC) | ❌ | ❌ | ❌ | ❌ | ✅* |
| SAML SSO | ❌ | ❌ | ❌ | ❌ | ✅* |
| 2FA | ❌ | ❌ | ❌ | ❌ | ✅* |
| Audit Logs | ❌ | ❌ | ❌ | ❌ | ✅* |
| Quotas | ❌ | ❌ | ✅ | ❌ | ✅* |
| Spam Protection | ❌ | ❌ | ✅ | ❌ | ✅* |
| Follow-ups | ❌ | ❌ | ✅ | ✅ | ✅ |
| Pretty URLs | ❌ | ❌ | ❌ | ✅ | ✅ |
| Projects Limit | 3 | 3 | Custom | 3 | Custom* |
*Depends on specific license feature flags
7. Recommendations for Refactoring
7.1 Unified Feature Access Layer
Create a single FeatureAccess service that abstracts the deployment type:
interface FeatureAccessService {
canAccessFeature(feature: FeatureKey, context: AccessContext): Promise<boolean>;
getLimit(limit: LimitKey, context: LimitContext): Promise<number>;
}
7.2 Normalize Feature Flags
Both Cloud and On-Premise should use the same feature flag schema. Cloud billing plans should map to predefined feature sets.
7.3 Remove License Requirement from Cloud
Cloud should not need ENTERPRISE_LICENSE_KEY. The license server should be bypassed entirely, with features controlled by billing plan.
7.4 Consider Feature Entitlements
Move to an "entitlements" model where:
- Cloud: Stripe subscription metadata defines entitlements
- On-Premise: License API returns entitlements
Both resolve to the same TFeatureEntitlements type.
8. Files That Would Need Changes
High Priority (Core Logic)
apps/web/modules/ee/license-check/lib/license.tsapps/web/modules/ee/license-check/lib/utils.tsapps/web/lib/constants.ts
Medium Priority (Feature Implementations)
- All files in
apps/web/modules/ee/*/actions.ts apps/web/modules/environments/lib/utils.tsapps/web/modules/survey/lib/permission.tsapps/web/modules/survey/follow-ups/lib/utils.ts
Lower Priority (UI Conditional Rendering)
- Settings pages with
IS_FORMBRICKS_CLOUDchecks UpgradePromptcomponent usages- Navigation components
9. Summary
The current implementation has organic complexity from evolving independently for Cloud and On-Premise deployments. A refactor should:
- Unify the feature access mechanism behind a single interface
- Simplify by removing the dual-check pattern
- Normalize feature definitions across deployment types
- Test with a cleaner mocking strategy
This would reduce the 100+ files touching enterprise features to a single source of truth, making the codebase more maintainable and reducing bugs from inconsistent feature gating.