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.

The usage 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 card progress 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 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
usage-card.tsx
"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 example
usage-meter.tsx
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>
  );
}