Skip to content

NSFW Toggle Behaviour

ADR: Always-visible NSFW toggle with context-dependent locking

Date: 2026-03-05 Linear: BEEF-1161 PRs: #2319 (toggle rework), #2302 (NSFW pricing + I2V/EiF locking)

Context

The NSFW toggle controls whether a generation request is processed through the NSFW pipeline. Prior to this change, toggle visibility and behaviour varied inconsistently across entry points — sometimes hidden entirely for certain media types (signatures, videos, carousels, KackMe), sometimes visible but with unclear defaulting.

The team raised concerns that this was confusing for users. Additionally, a compliance constraint exists: NSFW generations are routed through different payment processors than SFW ones. A user must not be able to submit an NSFW source image through the SFW pipeline, as this violates processor terms.

Decision

Visibility

The toggle is visible whenever both conditions are met:

  1. nsfw-onboarding feature flag is enabled (PostHog)
  2. User has completed KYC verification (isUserKycVerified)

If either condition is false, the component renders nothing.

Default state

On mount (and whenever the source media changes), the toggle defaults to:

isNsfw = mediaIsNsfw AND isSelectedCharacterNsfw

Both must be true — an NSFW source with a SFW character defaults to off, preventing accidental NSFW generation on a character that doesn’t support it.

Toggle locking

The toggle is visible but disabled (greyed out, tooltip: “Tied to the selected example”) in certain contexts. Locking is determined by isNsfwToggleLocked in useNsfwOnboarding:

ContextLocked?Why
Create page / blank gen (no flags, no initialPrompt)NoUser is starting from scratch, free to choose
Any media with initialPrompt set (explore examples, copy-prompt, gallery reuse)YesContent is tied to a specific example; NSFW state must match the source
Signature / video / carousel examplesYesSame as above — isSignature, isVideo, or isCarousel flag makes it non-simple
KackMeYesisCarousel flag is set, making it non-simple
Edit (EiF) with SFW source (non-free edit)NoUser may nudify a SFW starting image
Edit (EiF) with SFW source (free edit)YesUser may abuse credits
Edit (EiF) with NSFW sourceYesCan’t use SFW pipeline on NSFW source (compliance)
I2V with SFW sourceNoUser may nudify a SFW starting image
I2V with NSFW sourceYesCan’t use SFW pipeline on NSFW source (compliance)

The locking logic in code:

if (imageEditMode) return !!mediaIsNsfw; // locked only if source is NSFW
if (imageToVideoMode) return !!mediaIsNsfw; // locked only if source is NSFW
const isSimplePrompt =
!isVideo &&
!isSignature &&
!isCarousel &&
!imageToVideoMode &&
!imageEditMode &&
!initialPrompt;
if (isSimplePrompt) return false; // blank gen — unlocked
return true; // everything else — locked

Toggle interaction

When the user clicks the toggle:

  1. If locked → no-op (early return)
  2. If turning ON (checked=true) and a requiredAction exists → execute the action (paywall, character selector, etc.) instead of toggling
  3. Otherwise → set isNsfw to the new value

Turning OFF never triggers requiredAction — the if (checked && requiredAction) guard ensures this. Users can always disable NSFW without being gated.

Required actions (gating NSFW enable)

When the toggle is unlocked, enabling NSFW may still be blocked if the user hasn’t completed onboarding steps. getNsfwRequiredActionForCreate returns a callback that redirects the user:

ConditionAction
No NSFW-capable subscription (starter/plus tier)Paywall → upgrade to creator/ultimate
Has subscription, selected character is NSFW-onboardedundefined (proceed normally)
Has subscription, other NSFW characters availableToast + character selector
Has subscription, draft NSFW character existsFinish onboarding dialog

NSFW pricing

NSFW generations cost 5× credits (NSFW_CREDITS_MULTIPLIER = 5). This multiplier applies to all media types: images, videos, carousels. Pricing is calculated in getPromptCreditsPrice() based on promptData.isNsfw.

NSFW classification of source media

isMediaExampleNsfw(explicitnessLevel) returns true for any level other than SFW or SSFW. This means NSFW, EXPLICIT, and UNSPECIFIED are all treated as NSFW.

Subscription tiers

NSFW generation is gated behind nsfwSupported on the plan:

TierNSFW supported
StarterNo
PlusNo
CreatorYes
UltimateYes
UltinextYes

Rationale

Alternative 1 — Hide the toggle when state is inherited (previous behaviour). The toggle was invisible for signatures, videos, carousels, and KackMe. NSFW state was silently inherited from the source. Rejected because users couldn’t tell whether their generation was NSFW, leading to confusion and support requests.

Alternative 2 — Always allow toggling in both directions. Let users freely switch between SFW and NSFW regardless of source content. Rejected because allowing NSFW→SFW on an NSFW source image violates payment processor compliance — the SFW pipeline cannot process NSFW content.

Alternative 3 — Lock the toggle for all non-blank-gen contexts (initial PR #2319 approach). Every example, edit, and I2V flow locked the toggle. Refined in PR #2302 to unlock Edit/I2V when the source is SFW, since users should be able to opt into NSFW for their own SFW images.

The chosen approach maximises user control while enforcing the one hard constraint: NSFW source content cannot flow through SFW processors.

Consequences

Positive:

  • Users always see the toggle state — no more silent NSFW inheritance
  • Locked state is visually communicated (opacity, tooltip), reducing confusion
  • Compliance is enforced at the UI level — NSFW sources can’t be switched to SFW

Negative:

  • Edit/I2V locking is asymmetric (SFW→NSFW allowed, NSFW→SFW blocked), which may feel inconsistent to users unfamiliar with the compliance reason

Key files

FilePurpose
apps/brisket/src/features/create-image/components/nsfw-toggle/index.tsxToggle component — rendering, click handling, useEffect for default
apps/brisket/src/features/character-page-ccv4/hooks/use-nsfw-onboarding.tsxCore hook — availability, locking, required actions, subscription checks
apps/brisket/src/features/character-page-ccv4/helper-functions.tsxisMediaExampleNsfw — classifies explicitness levels
apps/brisket/src/utils/credits-price.tsgetPromptCreditsPrice — applies NSFW multiplier
apps/brisket/src/lib/constants.tsNSFW_CREDITS_MULTIPLIER = 5
apps/brisket/src/features/checkout/components/constants.tsPlan definitions with nsfwSupported flag