RegistryUI recipes
Usage meter
This period's usage per meter against the plan's quota, with the reset date.
Both sides call useUsageMeter from better-supabase/blocks/usage/react; the usage block must be installed (npx better-supabase add usage). Its page on bettersupabase.com is /recipes/usage-meter.
Web, with shadcn components:
npx shadcn add card progress skeleton"use client";
import { useUsageMeter } from "better-supabase/blocks/usage/react";
import { tenantOf } from "better-supabase/react";
import { useExtracted, useFormatter } from "next-intl";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { useAuth } from "@/lib/hooks";
import { blockConfig } from "@/lib/supabase/blocks.generated";
/** The API schema `gen` wrote for the usage block. */
const API_SCHEMA = blockConfig.usage.api;
/** This period's usage per meter against the plan's quota. */
export function UsageCard() {
const t = useExtracted("dashboard");
const format = useFormatter();
const organizationId = tenantOf(useAuth()) ?? null;
const { meters } = useUsageMeter({
organizationId,
meters: ["api.calls", "customers.created"],
schema: API_SCHEMA,
});
if (!organizationId) {
return null;
}
if (meters === undefined) {
return <UsageCardSkeleton />;
}
const labels = new Map([
["api.calls", t("API calls")],
["customers.created", t("Customers added")],
]);
return (
<Card data-testid="usage">
<CardHeader>
<CardTitle>{t("Usage this period")}</CardTitle>
<CardDescription>
{t("Counted by the usage module, limited by your plan.")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
{meters.map((meter) => (
<div key={meter.meter} className="space-y-2">
<div className="flex items-baseline justify-between gap-2 text-sm">
<span className="font-medium">
{labels.get(meter.meter) ?? meter.meter}
</span>
<span
className={
meter.exceeded
? "text-destructive tabular-nums"
: "text-muted-foreground tabular-nums"
}
>
{meter.limit === undefined
? t("{used} used, no limit", {
used: format.number(meter.used),
})
: t("{used} of {limit}", {
used: format.number(meter.used),
limit: format.number(meter.limit),
})}
</span>
</div>
<Progress value={(meter.fraction ?? 0) * 100} />
<p className="text-xs text-muted-foreground">
{t("Resets {date}", {
date: format.dateTime(
new Date(meter.resetsAt.epochMilliseconds),
{ dateStyle: "medium" },
),
})}
</p>
</div>
))}
</CardContent>
</Card>
);
}
export function UsageCardSkeleton() {
return <Skeleton className="h-64 rounded-xl" aria-busy="true" />;
}React Native, with ActivityIndicator, Text, View from react-native:
import { useUsageMeter } from "better-supabase/blocks/usage/react";
import { tenantOf, useAuth } from "better-supabase/react";
import { ActivityIndicator, Text, View } from "react-native";
import { API_SCHEMA } from "../lib/blocks";
const labels = new Map([
["api.calls", "API calls"],
["customers.created", "Customers added"],
]);
/** This period's usage per meter against the plan's quota. */
export function UsageMeter() {
const organizationId = tenantOf(useAuth()) ?? null;
const { meters } = useUsageMeter({
organizationId,
meters: ["api.calls", "customers.created"],
schema: API_SCHEMA,
});
if (!organizationId) {
return null;
}
if (meters === undefined) {
return <ActivityIndicator />;
}
return (
<View style={{ gap: 12, padding: 16, borderWidth: 1, borderRadius: 12 }}>
<Text style={{ fontSize: 17, fontWeight: "600" }}>Usage this period</Text>
{meters.map((meter) => (
<View key={meter.meter} style={{ gap: 4 }}>
<View style={{ flexDirection: "row" }}>
<Text style={{ flex: 1 }}>
{labels.get(meter.meter) ?? meter.meter}
</Text>
<Text style={{ color: meter.exceeded ? "#b91c1c" : "#737373" }}>
{meter.limit === undefined
? `${meter.used} used, no limit`
: `${meter.used} of ${meter.limit}`}
</Text>
</View>
<View
style={{ height: 6, borderRadius: 3, backgroundColor: "#e5e5e5" }}
>
<View
style={{
height: 6,
borderRadius: 3,
backgroundColor: meter.exceeded ? "#b91c1c" : "#111",
width: `${(meter.fraction ?? 0) * 100}%`,
}}
/>
</View>
<Text style={{ color: "#737373", fontSize: 12 }}>
Resets{" "}
{meter.resetsAt.toLocaleString(undefined, { dateStyle: "medium" })}
</Text>
</View>
))}
</View>
);
}Last updated on
Plan picker
The plans with their prices and a checkout button each. Checkout starts in a server action or API route that holds the Stripe key, and the client opens the URL it returns.
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.