All recipes

Recipe

Feature gate

Content shown only when the active organization's plan includes a feature, read from the session's features claim and updated when the session refreshes. Web and native call useFeatureGate from better-supabase/blocks/entitlements/react.

The entitlements blockWebReact Native

1.What you build

No screenshot yet. Run the Next.js example (pnpm dev:example) to see it.

2.Add the components

Web: shadcn

terminal
npx shadcn add button skeleton

Native: React Native

The snippet uses ActivityIndicator, Text, View from react-native, so it needs no styling library. For shadcn-style native components, React Native Reusables serves them from a registry the shadcn CLI reads.

3.Install the glue

Install the block with npx better-supabase sql add entitlements. The glue items are copied into your app, so edit them as you need.

Web

None: the hook calls the block from the client.

Native

None: the hook calls the block from the client.

4.Write the component

Web: React and shadcn

View in the example
feature-gate.tsx
"use client";

import type { ReactNode } from "react";

import { useFeatureGate } from "better-supabase/blocks/entitlements/react";
import { LockIcon } from "lucide-react";
import { useExtracted } from "next-intl";

import type { Entitlement } from "@/lib/claims";

import { buttonVariants } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Link } from "@/i18n/navigation";

/**
 * Renders `children` only when the active organization's plan includes
 * `feature`, read from the session's `features` claim; otherwise a link to
 * the plans. UX only: RLS and the route behind it check the feature too.
 */
export function FeatureGate({
  feature,
  children,
}: {
  feature: Entitlement;
  children: ReactNode;
}) {
  const t = useExtracted("billing");
  const { allowed } = useFeatureGate(feature);
  if (allowed === undefined)
    return <Skeleton className="h-8 w-28" aria-busy="true" />;
  if (allowed) return children;
  return (
    <Link
      href="/settings/billing"
      className={buttonVariants({ variant: "outline", size: "sm" })}
      data-testid={`upgrade-${feature}`}
    >
      <LockIcon />
      {t("Upgrade to unlock")}
    </Link>
  );
}

Native: React Native

View in the example
feature-gate.tsx
import type { ReactNode } from "react";

import { useFeatureGate } from "better-supabase/blocks/entitlements/react";
import { ActivityIndicator, Text, View } from "react-native";

/**
 * Renders `children` only when the active organization's plan includes
 * `feature`, read from the session's `features` claim. UX only: RLS checks
 * the feature too.
 */
export function FeatureGate({
  feature,
  children,
}: {
  readonly feature: string;
  readonly children: ReactNode;
}) {
  const { allowed } = useFeatureGate(feature);
  if (allowed === undefined) return <ActivityIndicator />;
  if (allowed) return children;
  return (
    <View style={{ padding: 12, borderWidth: 1, borderRadius: 12 }}>
      <Text style={{ fontWeight: "600" }}>Not in your plan</Text>
      <Text style={{ color: "#737373" }}>
        Upgrade to a plan with {feature} to use this.
      </Text>
    </View>
  );
}