Require a plan to use the app
Section titled “Require a plan to use the app”Some apps shouldn’t do anything until the workspace (called a tenant in the API) is on a plan. “Requiring a plan” means blocking the app until the current workspace has an active plan, and letting it through the moment one exists.
A plan counts as active once the workspace has either:
- selected a free plan (instant, no payment involved), or
- completed Stripe Checkout for a paid plan (a payment method is captured).
Under the hood the gate keys off a single flag on the subscription status: shouldSelectPlan. While it’s true the workspace has no active plan and the app should stay blocked; once a plan is selected or checked out it flips to false and the app opens up. You never compute this yourself; Bridge derives it from the workspace’s billing state. (This is the onboarding gate. A workspace that had a plan and lost it, say after exhausted payment retries, is billing-locked instead, which is a separate signal. See How billing works for how the two relate.)
There are two ways to enforce the gate. Lead with <BridgePaywall>, the default, blessed approach, and reach for the config paywall route when you’d rather the plan picker be a real routed page than a modal overlay.
Method 1: <BridgePaywall> (recommended)
Section titled “Method 1: <BridgePaywall> (recommended)”<BridgePaywall> is a hard gate you wrap around your app: it blocks everything until a plan is active, with no shouldSelectPlan checks or redirects to wire yourself. Put it in your root app/layout.tsx and pass your app as children.
While shouldSelectPlan is true it renders a full-screen modal with a <PlanSelector> inside; otherwise it renders its children (your app).
// app/layout.tsx
import { BridgePaywall } from '@nebulr-group/bridge-nextjs/client';
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>
<BridgePaywall successRedirect="/welcome" cancelRedirect="/plans">
{/* your app: only rendered once a plan is active */}
{children}
</BridgePaywall>
</Providers>
</body>
</html>
);
}
Tip: Pass your app as
children.<BridgePaywall>renders its children only when a plan is active, so nothing behind the gate mounts until then.
Framework note:
<BridgePaywall>is a client component, but thechildrenyou pass through from a server layout can still be Server Components; React carries them across the boundary as already-rendered trees.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| successRedirect | string | '/' | Where to send the user after a successful Stripe payment |
| cancelRedirect | string | '/' | Where to send the user if they cancel checkout |
| onSelect | ({ plan, price }) => void | (none) | Called after free-plan selection or a direct plan change (not the Stripe redirect path); use for analytics side-effects |
| heading | ReactNode | “Choose a plan” | Override the modal heading |
| children | ReactNode | (none) | Your app. Rendered only once a plan is active |
What the user sees: a workspace with no plan lands on a full-screen modal with the plan picker and cannot get past it. The instant they pick a plan (or return from checkout), the modal disappears and your app renders in its place.
Method 2: config paywall route
Section titled “Method 2: config paywall route”Prefer this when you want the plan picker to be a real routed page rather than a modal overlay, for example a dedicated /plans onboarding step with its own layout, copy, and URL you can link to.
Set billing.paywallRoute in the BridgeConfig you pass to <BridgeProvider> in your providers component:
// app/providers.tsx
'use client';
import { BridgeProvider } from '@nebulr-group/bridge-nextjs/client';
import { useMemo, type ReactNode } from 'react';
export function Providers({ children }: { children: ReactNode }) {
// appId comes from NEXT_PUBLIC_BRIDGE_APP_ID via the provider's env reader
const config = useMemo(
() => ({
billing: {
paywallRoute: '/plans',
},
}),
[],
);
return <BridgeProvider config={config}>{children}</BridgeProvider>;
}
Then render a <PlanSelector> at that route:
// app/plans/page.tsx
'use client';
import { PlanSelector } from '@nebulr-group/bridge-nextjs/client';
export default function PlansPage() {
return <PlanSelector successUrl="/welcome" cancelUrl="/plans" />;
}
<BridgeProvider> handles the gate for you: it checks the subscription status, and if the authenticated workspace still needs to pick a plan it issues a redirect to paywallRoute. It only redirects when all of the following hold, so there’s no redirect loop and no gate on exempt workspaces:
billing.paywallRouteis configured- the current path isn’t already the paywall route
- the workspace is authenticated but has
shouldSelectPlan: true - the workspace hasn’t opted out via
paymentsAutoRedirect: false
Framework note: the redirect runs client-side, after hydration, on every route change; it doesn’t block Next.js server rendering. If a page must never reach the browser without a plan, enforce it on your backend too (see Check plans on your backend).
Tip:
<PlanSelector>is the same picker<BridgePaywall>renders inside its modal. See Choose & switch plans for its full prop table and customization options.
The end-to-end flow
Section titled “The end-to-end flow”Both methods drive the same underlying flow:
- A user signs in to a workspace that has no active plan →
shouldSelectPlanistrue. - The gate engages: the
<BridgePaywall>modal appears, or<BridgeProvider>redirects to yourpaywallRoutepage. - The user picks a plan from the
<PlanSelector>:- Free plan → activated instantly, no payment.
onSelectfires and the store refreshes. - Paid plan → the user is sent to Stripe Checkout to capture a payment method.
- Free plan → activated instantly, no payment.
- On successful payment the user returns to your app at
successRedirect; if they cancel, they land oncancelRedirect. - With a plan now active,
shouldSelectPlanflips tofalse→ the gate opens and your app renders.
Opting out: paymentsAutoRedirect: false
Section titled “Opting out: paymentsAutoRedirect: false”paymentsAutoRedirect is a flag on the subscription status. When it’s false, the workspace has opted out of the platform’s native plan-selection gate; such workspaces are exempt from the automatic block. Both methods above respect it: <BridgePaywall> renders its children instead of the modal, and <BridgeProvider> skips the paywall redirect entirely.
This exists so certain workspaces can bypass the forced plan choice, for example accounts provisioned or billed out-of-band, where forcing a plan selection in the app would be wrong. Those workspaces still reach your app normally; you’re free to render your own <PlanSelector> where it makes sense, but the platform won’t block them for you.
successRedirect and cancelRedirect are independent of this flag; they’re simply where the user lands after leaving Stripe Checkout (success or cancel, respectively). They default to '/' on <BridgePaywall>.