Gates
Four questions, four components — and which one you actually want.
A gate answers one question: may this render right now, and if not, what does the user see instead?
There are four, they ask genuinely different questions, and picking the wrong one is how a screen ends up blocked for a reason the user cannot act on.
| Gate | Question | Blocked by |
|---|---|---|
ConnectAccessGate | May this workspace use the application at all? | Suspended, no plan, onboarding pending, payment overdue |
ConnectSubscriptionGate | Is there a live subscription? | No subscription, or a cancelled one |
ConnectFeatureGate | Does their plan include this feature? | Feature not in the plan's features |
ConnectWorkspaceTypeGate | Is this the right kind of workspace? | Individual account on a business-only screen |
The access gate
The broad one, and the only one you get without asking — AppShell
wraps its children in it.
It covers four unrelated failures that all mean "not right now", and it renders
a different, actionable screen for each: a suspended workspace gets a support
route, an overdue payment gets a link to billing, pending onboarding gets a
link to finish it. That last detail is the reason to use the gate rather than
your own if: a blocked screen has to tell the user what to do, and four
branches of that are four chances to write a dead end.
import { ConnectAccessGate } from "@cosmoops/connect-sdk/client";
<ConnectAccessGate>
<Dashboard />
</ConnectAccessGate>Every state it can be in, on one control — including the two that are easy to forget you have never looked at, the loading frame and the signed-out one:
ConnectAccessGate
One gate, every reason a workspace cannot use your app right now.
import { ConnectAccessGate } from "@cosmoops/connect-sdk/client";
<ConnectAccessGate>
<Dashboard />
</ConnectAccessGate>The advisory half is separable. ConnectReadinessBanner says what is wrong
without blocking anything, which is what a payment retrying inside its grace
period deserves — the workspace is still a customer, and the screen still
works:
ConnectReadinessBanner
The advisory half: says something is wrong, blocks nothing.
import { ConnectReadinessBanner } from "@cosmoops/connect-sdk/client";
<ConnectReadinessBanner />Exempting a route
Some routes must render while blocked — the billing page most obviously, since that is where the user goes to unblock themselves.
Declare it on the nav item, next to the route it applies to:
const NAV = [
{ label: "Billing", href: "/billing", icon: <CreditCard className="size-5" />, gateExempt: true },
];For a route that is in neither nav — a payment callback, say — use
gateExemptRoutes, which is prefix-matched the same way:
<AppShell nav={NAV} gateExemptRoutes={["/checkout/callback"]}>
{children}
</AppShell>Exempt the route that fixes the problem
A payment-overdue gate over your billing page is a closed loop: the user cannot pay because they have not paid. Whatever route resolves each blocker has to be exempt.
Reading the verdict yourself
When you want the decision but not the default screen:
import { useConnectAccessGate } from "@cosmoops/connect-sdk/client";
const { blocked, reason } = useConnectAccessGate();
if (blocked) return <MyOwnEmptyState reason={reason} />;The feature gate
Per-feature, read from the subscribed plan's features.
import { ConnectFeatureGate } from "@cosmoops/connect-sdk/client";
<ConnectFeatureGate feature="advanced-reporting" fallback={<UpgradePrompt />}>
<AdvancedReports />
</ConnectFeatureGate>The hook form is usually better inside a component that is already rendering — hiding one button does not need a whole gate:
const canExport = useConnectFeature("csv-export");
<Button isDisabled={!canExport}>Export CSV</Button>Disable, don't hide
A hidden feature is indistinguishable from a missing one, and the user never learns an upgrade would give it to them. Disable it with a reason; hide only what would be meaningless to them.
ConnectFeatureGate
One screen, gated on a plan feature key.
import { ConnectFeatureGate } from "@cosmoops/connect-sdk/client";
<ConnectFeatureGate feature="reporting">
<DeliveryReports />
</ConnectFeatureGate>The subscription gate
Narrower than the access gate: it asks only about the subscription, ignoring verification, onboarding and payment state.
Reach for it on a screen that is about subscribing — a pricing page, an upgrade flow — where the access gate's "you have no plan" screen would be telling the user something they are already in the middle of fixing.
<ConnectSubscriptionGate fallback={<ConnectPlansPage />}>
<BillingSettings />
</ConnectSubscriptionGate>ConnectSubscriptionGate
For a screen that is meaningless without a subscription.
<ConnectSubscriptionGate>
<BillingSettings />
</ConnectSubscriptionGate>The workspace-type gate
Some screens only make sense for a team: a members table, seat management, an
invite flow. An individual workspace has exactly one seat and can never have
another, so those screens are not "empty" for them — they are meaningless.
<ConnectWorkspaceTypeGate type="business" fallback={<ConvertToBusinessPrompt />}>
<TeamSettings />
</ConnectWorkspaceTypeGate>A workspace can convert from individual to business under a live session, so read it per render rather than caching it past sign-in.
ConnectWorkspaceTypeGate
For a screen that only means anything to a team.
<ConnectWorkspaceTypeGate type="business" fallback={<ConvertToBusinessPrompt />}>
<TeamSettings />
</ConnectWorkspaceTypeGate>Choosing
Walk down this list and stop at the first that fits:
Is the whole application unusable right now?
ConnectAccessGate — and it is already on, so there is nothing to do.
Is this one screen about a capability their plan may not include?
ConnectFeatureGate, or useConnectFeature for a single control.
Is this screen only meaningful with a subscription, and is it not the page that sells one?
ConnectSubscriptionGate.
Does this screen only make sense for a team?
ConnectWorkspaceTypeGate.
None of the above?
No gate. Most screens need none — the access gate in the shell has already answered the only question that applies to all of them.