Skip to main content

Context

Custom CSS and dark mode touch the server, the survey renderer, the styling editors and five SDK packages. Mobile apps and self-hosted servers upgrade on their own schedule, so mixed versions are normal, not an edge case. This page is the release checklist; the customer-facing behavior lives in Custom CSS and SDK Compatibility.

What Ships Where

Deployment Order

Data and renderer first, so nothing a creator saves reaches a renderer that does not understand it yet.
  1. Server and renderer. Deploy the migration, processor, API, editor cards and renderer together. Existing surveys must look exactly the same: nothing is saved yet.
  2. Purge the CDN for /js/surveys.umd.cjs and /js/formbricks.umd.cjs. Both URLs are unversioned and cached for up to 30 days at the edge, so without a purge some apps and websites keep the old renderer for weeks. Repeat the purge after any renderer rollback.
  3. Stripe entitlement on Cloud, right after the purge (see below). Until it is attached, every Cloud organization sees the upgrade prompt in the editor cards, Scale included.
  4. SDK packages: @formbricks/js, then the iOS, Android, React Native and Flutter SDKs, each from its own repository. Publication does not have to be atomic. Record every released version in SDK Compatibility, replacing “Pending release”. Customers whose apps use the mobile SDKs need to ship an app update before their Custom CSS shows in the app; tell them early.
  5. Self-hosted gets everything with the release that contains steps 1 and 2. There is no plan check, flag or environment variable. Add one release-note line: Custom CSS styles the card; Custom Head Scripts keep scripts, tags and page-level styles.

Before Enabling On Cloud

  • Stripe entitlement. Attach the entitlement feature with lookup key custom-css to the Scale and Custom products. This is the only gate on Cloud: without it, saving Custom CSS fails with “plan required” for every organization, Scale included. Self-hosted instances skip the plan check.

Turning It Off

There is no feature flag, so stopping Custom CSS everywhere takes a deploy. Switching off is not instant: each SDK keeps its configuration until it refreshes (up to 30 minutes on the web, up to an hour in the mobile SDKs). A processor version bump marks every stored compiled output as stale. Delivery reprocesses stale CSS from its saved source; if the current processor rejects it, the CSS is withheld (never the old output) and creators see a warning in the editor. BLOCK_EXTERNAL_CUSTOM_CSS_RESOURCES in apps/web/modules/custom-css/processor is a code constant, deliberately without an environment variable or setting. It is part of the effective processor version, so changing it also requires the bump.

Smoke Checks

Run before each step above:
  1. Existing surveys look exactly the same, in light mode, with no Custom CSS saved.
  2. Old SDK with the new renderer: a survey opens and answers save, on web, iOS and Android. Mobile shows no Custom CSS.
  3. New SDK with an old self-hosted server: surveys stay light, with no errors.
  4. A survey with saved CSS and Add custom styles off: respondents get the workspace CSS only; turning it back on brings the survey’s CSS back.
  5. Self-hosted: existing Custom Head Scripts still run.
  6. A CMS-style stylesheet (synthetic, never a customer’s file) shows one removed-import warning, and its ported hooks style the preview and the live survey in light and dark.

SDK Handoff

Every mobile SDK must pass customCss: { workspace, survey } explicitly, from workspaceSettings.customCss and each survey.customCss, and feature-detect formbricksSurveys.setAppearance. The renderer applies Custom CSS only from that prop, so an SDK that does not pass it renders no Custom CSS at all rather than half of it.

Styling Contract

The data-fb-part hook names are declared once, in FB_PART (packages/survey-ui/src/lib/parts.ts), exported from @formbricks/survey-ui and, without any components, from @formbricks/survey-ui/parts. Customers’ stylesheets depend on these names: renaming or removing one is a breaking change. parts.test.ts fails when the customer docs and the constant disagree.