All recipes
Recipe
Usage meter
This period's usage per meter against the plan's quota, with the reset date. Web and native call useUsageMeter from better-supabase/blocks/usage/react.
1.What you build
No screenshot yet. Run the Next.js example (pnpm dev:example) to see it.
2.Add the components
Web: shadcn
npx shadcn add card progress skeletonNative: 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 usage. 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"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-muted-foreground text-xs">
{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" />;
}
Native: React Native
View in the exampleimport { 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>
);
}