Embedded Components
Gating: Paywall, Gate, Show
Three renderings of one decision — Kaiten's wall, a render-prop with the numbers, a declarative switch — the five reasons a wall is up, the loading and error policies, and soft limits on the Kaiten Sushi Shop cast.
Gating is where the contract meets the interface: a feature the License does not grant, a quota the customer has exhausted, a flag that is off for them. The SDK offers three components for it, ordered from the most to the least opinionated. Paywall renders Kaiten's wall over your content. Gate renders nothing and hands you the numbers. Show renders its children or a fallback. All three read the same decision; they differ in what they do with it while the data loads and when a read fails.
The decision anticipates the server. Usage and metering describes the rule the API applies to a usage report — the grant's value, its overage allowance, the ceiling where reports are refused — and the SDK reproduces that arithmetic on the snapshot so the interface closes where the server will. The server stays the enforcement point; the guide gate a feature by License builds that server-side chain on the same cast.
The provider
Every gating component needs a KaitenProvider above it, scoped to one customer: only a customer's scope has a customer's entitlements. In your customers' browsers, that is mode="customer-session", through a customer session your backend mints for the signed-in customer and the Instance they use:
import { KaitenSessionClient } from "@kaitencloud/client";
import { KaitenProvider } from "@kaitencloud/react";
// Once, outside render. Your endpoint mints the signed-in customer's session —
// for Ninja Osaka, bound to ninja-osaka-prod — and returns { token, expiresAt }.
const sessionClient = new KaitenSessionClient({
apiUrl: KAITEN_API, // your organization's API base URL
getCustomerSession: () => fetch("/billing/session", { method: "POST" }).then((r) => r.json()),
});
<KaitenProvider mode="customer-session" sessionClient={sessionClient}>
<App />
</KaitenProvider>A customer session gates on the Instance's effective entitlements — the License's grant as add-ons, voucher boosts, and entitlement exceptions leave it: the values the server enforces. It carries no flags, so under it a flagKey reads as off (flag_disabled): gate on the entitlement there. A surface whose users are members of your organization runs in authenticated mode instead, on the licensing snapshot of one customer and Instance, flags included:
<KaitenProvider
mode="authenticated"
apiUrl={KAITEN_API}
accessToken={getToken} // a JWT your gateway trusts — never a ksh_ token in a browser
customerId="ninja-osaka"
instanceId="ninja-osaka-prod"
>
<App />
</KaitenProvider>customerId accepts the customer's slug or id. instanceId selects one of the customer's instances; omitted, the first by slug is used. Two customers of the Kaiten Sushi Shop cast run two instances — the first by slug would be ninja-osaka-delivery for Ninja Osaka and sakura-tokyo-demo for Sakura Tokyo — so the examples on this page name the instance: ninja-osaka-prod, sakura-tokyo-prod, demo-restaurant-prod. A customer session names its Instance when your backend mints it. Under public or static — a catalogue, no customer — Gate and Paywall stay open, with a warning in development. The credentials are on the section index.
One decision, five reasons
The decision has two layers. Access — useEntitlementCheck(slug) — answers whether the customer may use the entitlement right now: a License outside its term denies everything, a BOOLEAN must be true, an unlimited NUMBER always passes, a capped NUMBER passes while usage is below the ceiling the server enforces (the value plus its allowance), a CONFIG passes when present. Display — usePaywallTrigger(slug, { threshold, flagKey, min, onError }) — turns that answer into should a wall be shown, and why. The reason is what the components expose, as the data-reason attribute on Paywall and the reason field of Gate:
| Reason | Meaning | On the cast |
|---|---|---|
over_limit | Usage has reached the ceiling the server enforces. The next report is refused. | Ninja Osaka (Starter), menu-items at 5 of 5 — the grant tolerates no overage, so the wall is up at the fifth item. |
near_limit | Usage has passed threshold (default 0.8) of the value bought; the entitlement is still served. | Sakura Tokyo (Premium), menu-items at 42 of 50 — 84 %. Ninja Osaka, monthly-orders at 18 of 20 in the current calendar-month window — 90 %. |
not_entitled | The License does not grant the entitlement — a BOOLEAN at false, a slug the grant does not carry — or the License is outside its term. | Ninja Osaka, delivery-tracking granted false on Starter. |
flag_disabled | A flagKey was given and the flag evaluates off for this customer. | delivery-tracking-enabled for Ninja Osaka — its only rule reads the delivery-tracking grant. |
error | The snapshot could not be read. The wall is the fail-closed policy, not a decision about this customer. | The API unreachable, or the credential rejected. |
Reasons are evaluated in that order once the data is loaded: an unreadable snapshot is error before anything else, a disabled flag wins over the entitlement, a denied entitlement is over_limit when the meter reads 100 % or more and not_entitled otherwise, and only then does the threshold apply.
Two facts are worth holding onto. The meter and the door read different numbers. The percentage is usage over the value bought, so a customer who has consumed exactly what they purchased reads 100 %; the door closes at the ceiling, which is higher on a soft limit. Demo Restaurant (Standard, 10 % allowance) at 10 of 10 menu-items shows a meter at 100 % under a near_limit wall and is still served its eleventh item; at 11 the reason becomes over_limit, exactly where the API begins to answer 409. An unlimited grant is not metered. Sakura Tokyo's locations is unlimited on Premium: metered is false, no meter is drawn, no wall is ever raised for it — the same is true for a BOOLEAN, a CONFIG, or a flag refusal, where a 0 % bar would describe nothing.
min refines access for an action that consumes several units at once: min={5} keeps the door open only while at least five units remain below the ceiling.
Loading and errors
The three components share the decision and split on policy:
| Component | While loading | On a data error |
|---|---|---|
Paywall | Reveals nothing — renders loading (nothing by default) until access is known | Wall up, reason: "error" — unless onError="allow" |
Show | Renders loading | The onError policy applies whatever when says, because the condition cannot be evaluated |
Gate | Open — allowed: true, nothing metered | blocked: true unless onError="allow" |
Gate is open while loading and open outside any provider on purpose: a spinner on every guarded button costs more than letting through a call the API will reject on its own, and a product that has not wired Kaiten yet must not gate itself. Paywall and Show protect content that has a price, and reveal nothing they are not sure about. Outside a provider, Paywall and Show throw — a component that silently answered "not allowed" would gate features by accident.
The client sends a read again, with a backoff, when it failed on the network or with a 5xx; past those attempts, nothing in the SDK retries on its own — a failed License read must not turn every gate into a retry loop. The data last read stays, with error set, until the next poll or a refetch. KaitenFailed exposes retry so the host can offer one.
Paywall
import { Paywall } from "@kaitencloud/react-ui";
<Paywall
entitlement="menu-items"
threshold={0.8}
onUpgrade={() => navigate("/settings/license")}
>
<MenuEditor />
</Paywall>Ninja Osaka's menu editor is gated on menu-items. At 3 of 5 the editor renders; at 4 the soft wall rises with data-reason="near_limit" and can be dismissed; at 5 it reads over_limit, the meter at 100 %, with the button — because onUpgrade is wired.
| Prop | Default | Effect |
|---|---|---|
entitlement | — | The entitlement slug the wall watches. |
threshold | 0.8 | Where near_limit starts, as a fraction of the value bought. Set it to the entitlement's warning threshold when they should agree. |
min | — | Units that must remain for the door to stay open. |
flagKey | — | A flag that must be on as well; off, the wall shows flag_disabled. |
mode | "soft" | soft: an overlay on the gated area, with a dismiss button that reveals the content and re-arms when the wall would rise again. hard: a native modal dialog with a focus trap; Escape and the close button close the dialog and leave the inline wall in place, so the feature stays blocked while the page stays usable. |
onError | "block" | "allow" renders the children on a failed read. |
loading | null | What to render while access is unknown. |
onUpgrade | — | Wires the call to action. Without it there is no button — a CTA is never a dead end. |
onDismiss, onThresholdReached(percentage) | — | The dismiss of a soft wall; one call each time the threshold is crossed, re-armed when the wall disappears. |
The content behind a wall is inert: not focusable, hidden from assistive technology, in both React 18 and 19. The wall states its reason to assistive technology too, and moves focus onto its first element, restoring it when it closes. A wall over a one-line element renders compact; a usageWindow line under the meter distinguishes at 100 %, buy more from at 100 %, wait for the window to reset — monthly-orders resets each calendar month, menu-items never does. The button's label follows the recommended action: add quota for a limit problem, upgrade for an access problem.
Under a customer session, the wall also knows the subscription. It carries its state in data-subscription, adds a sentence when the subscription explains the wall — a trial or a cancellation about to end, a failed payment, a subscription that is over, a License bought that starts once its payment is confirmed —, and offers Subscribe rather than Upgrade to a customer whose portal says they can buy a License by themselves. A subscription that is past due closes no wall: billing status removes no access.
examples/storefront, route /gating, signed in as Ninja Osaka (Starter) on instance ninja-osaka-prod, slug menu-items at 5 of 5. Must show: the soft wall over the gated area with the meter at 100 %, the "add quota" button, and — in the browser inspector beside it — the data-slot="paywall" element carrying data-reason="over_limit".Gate
import { Gate } from "@kaitencloud/react";
<Gate entitlement="monthly-orders">
{({ allowed, warning, metered, current, limit, percentage, reason }) => (
<button disabled={!allowed} title={reason ?? undefined}>
New order{metered ? ` — ${current} / ${limit}` : null}
</button>
)}
</Gate>Gate passes a GateState to its children and renders nothing else: allowed, blocked (over_limit, not_entitled, flag_disabled, and error under the default policy), warning (near_limit), reason, current, limit, percentage, metered, isLoading, error. It accepts entitlement, flagKey, min, threshold, and onError. For Ninja Osaka on ninja-osaka-prod the button above reads New order — 18 / 20 and stays enabled; for Sakura Tokyo on sakura-tokyo-prod, 730 / 1000. When metered is false the numbers describe nothing — draw no counter. Under a customer session the state also says where the limit comes from: source is the layer that grants the entitlement, license or addon, and provenance how it reached its value — the License's grant, the add-ons attached, the voucher boosts and the arithmetic — enough for a line such as (1,000 base) + 200 voucher = 1,200.
Show
import { Show } from "@kaitencloud/react";
<Show when="entitled" entitlement="delivery-tracking" fallback={<DeliveryTrackingTeaser />}>
<DeliveryMap />
</Show>
<Show when="flag-enabled" flagKey="delivery-tracking-enabled">
<DeliveryMap />
</Show>when is one of "entitled", "not-entitled" — which require entitlement, and accept min — or "flag-enabled", "flag-disabled" — which require flagKey. A mismatched pair is a compile-time error. Sakura Tokyo (Premium, delivery-tracking granted true) sees the map in both examples; Ninja Osaka sees the teaser in the first and nothing in the second. The two are the same decision taken at two points: the entitlement is the commercial fact, the flag is the runtime switch that reads it — the gate a feature by License guide shows why keeping both matters.
Reporting usage from the interface
useReportUsage() reports a unit of usage from the browser — mutate({ entitlementSlug: "monthly-orders", input: { value: { type: "number", value: 1 }, behavior: "append" } }) — and moves the meters optimistically: the bar advances at once, holds during the call, revalidates against the server on success, and rolls back on refusal. A report without a key is sent exactly once: a replayed report is double-counted usage, and usage is the billing base. Give input a transactionId and the API applies the report once per key: the key travels in the Idempotency-Key header, and a keyed report that fails on the network or with a 500, 502, 503 or 504 is sent again under the same key, up to three attempts — once the client has seen the server echo a key back. A key reused for another report rejects with TransactionIdReusedError (idempotent usage reports). Only authenticated mode reports from the browser: a customer session has no usage route. In production the report usually comes from your backend — the meter usage guide covers both.
examples/storefront, signed in as Demo Restaurant (Standard) on demo-restaurant-prod. Route /usage: report menu-items (aggregation LATEST — the reported number is the new count) with value: 8 — the meter moves from 6 to 8 of 10 optimistically. Switch to /gating on slug menu-items: the near_limit wall is up at 80 %. Back on /usage: report value: 11 — the meter reads 110 %, accepted inside the 10 % allowance; on /gating the wall now reads over_limit. Finish with a report of value: 12, refused by the server, and the meter rolling back to 110 %.Where to go next
Portal & pricing
What the customer sees when they open their License — the portal, its billing sections, the meters — and the pricing table a checkout starts from.
Gate a feature by License
The server-side chain a gate anticipates: the BOOLEAN entitlement, two grants, the flag that reads them, two evaluations in the audit trail.
Usage and metering
The enforcement rule the SDK reproduces — value, overage allowance, ceiling — and periodic usage windows.
Overview
React components that render, inside your product and on your site, what Kaiten knows — a customer's License, quotas and flags, your public catalogue, the checkout that sells a License, and the portal a customer manages it from. Headless core, styled components, four provider modes, one appearance system, one browser boundary.
Checkout
CheckoutFlow for a customer you know, through a customer session; AnonymousCheckoutFlow for a visitor with no account; CheckoutSummary, TrialBanner and PaymentMethodCard around them — the server prices, the payment provider's page takes the card, and a paid License applies once it is paid.