← All notes
Frontend Development3 min

Feature gates that flash 'upgrade' before the real UI loads

If your entitlement query is still loading, do not render the upgrade wall. Defaulting a feature gate to locked creates a one-frame (or one-second) lie: allowed users see 'Available on Professional' before the real Owners list appears. Treat unknown as pending, not denied.

The flash is a product bug

We hit this on a construction admin surface: the Owners route briefly showed a Core-plan upsell, then swapped to the real account list. From the outside it looked gated. From the code it was a race — plan status arrived after the page shell.

Users do not experience your loading waterfall. They experience the first confident UI. An upgrade card is a confident UI. So is an empty state. A skeleton is less confident, and that is the point.

Unknown is not denied

Three states matter: allowed, denied, and pending. Most gates only model two, and pending collapses into denied because the boolean starts false. That is how you get the flash.

Pending should render a neutral placeholder — skeleton rows, a quiet spinner in the content well, nothing that names a plan tier. Denied renders the upsell. Allowed renders the tool. Crossing from denied to allowed without user action feels broken even when it 'recovers.'

Where to put the wait

Prefer waiting above the gate, not inside it. If the shell already knows the plan from a parent layout query, children should not re-default to locked while they refetch. Share the entitlement through context or a cached server payload so deep links do not reintroduce the flash.

If you must fetch on the page, keep the previous route's chrome and replace only the main pane with a skeleton that matches the densest version of the page — list cards, not a marketing upsell.

Nav locks lie too

A padlock icon next to Owners while the same session can open Owners is the same class of bug. Either the nav knows the plan, or it should not decorate with locks until it does. Decorative locks trained on a loading default teach users the feature is unavailable.

We now treat entitlement flashes the way we treat reduced-motion bugs: as correctness. If QA can screenshot the wrong state, customers will believe the wrong state.

Questions

Why does a feature briefly show an upgrade screen for entitled users?
The gate usually defaults to locked while the plan query loads. For a moment the UI asserts denial. Entitled users see a paywall flash before the real view replaces it.
What should render while entitlements are loading?
A pending state — skeletons or a quiet placeholder — not the upgrade wall. Only render the upsell after you know the account is denied.
How do you keep deep links from reintroducing the flash?
Resolve entitlements in a parent layout or shared cache so child routes do not re-initialize as locked. If the page must fetch, wait above the gate before choosing upsell versus feature.

Sources

  1. BuilderHelp case study
  2. React — SuspenseUseful mental model: do not commit a denied UI while data is still pending.
  3. web.dev — Avoid large layout shiftsPaywall-to-content swaps are a trust problem as much as a layout one.

Have something to build?

Tell us what you're working on and we'll tell you honestly whether we're the right fit.

Work with us