All recipes
Recipe
Notification list
The caller's latest notifications with an unread marker, loaded again on every change. Web and native call useNotifications from better-supabase/blocks/notifications/react.
1.What you build

2.Add the components
Web: shadcn
npx shadcn add badge empty skeletonNative: React Native
The snippet uses ActivityIndicator, FlatList, 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 notifications. The glue items are copied into your app, so edit them as you need.
Web
npx shadcn add @better-supabase/notifications-actions-nextNative
npx shadcn add @better-supabase/blocks4.Write the component
Web: React and shadcn
View in the example"use client";
import { useNotifications } from "better-supabase/blocks/notifications/react";
import { BellIcon } from "lucide-react";
import { useExtracted, useFormatter } from "next-intl";
import { Badge } from "@/components/ui/badge";
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { Skeleton } from "@/components/ui/skeleton";
import { useAuth } from "@/lib/hooks";
import type { NotificationRow } from "../notification-types";
import { loadNotifications } from "../notifications-actions";
async function loadRows(): Promise<readonly NotificationRow[]> {
const result = await loadNotifications({});
return result.ok ? result.data : [];
}
/**
* The caller's latest notifications, loaded again after each broadcast on
* their private topic `notifications:<user id>`: a new one, or one marked read.
*/
export function NotificationList() {
const t = useExtracted("notifications");
const format = useFormatter();
const auth = useAuth();
const userId = auth.user?.id;
const { items } = useNotifications({
topic: userId ? `notifications:${userId}` : null,
load: loadRows,
});
if (items === undefined) return <NotificationListSkeleton />;
if (items.length === 0) {
return (
<Empty className="border">
<EmptyHeader>
<EmptyMedia variant="icon">
<BellIcon />
</EmptyMedia>
<EmptyTitle>{t("Nothing here yet")}</EmptyTitle>
<EmptyDescription>
{t("Send yourself a notification to see it arrive.")}
</EmptyDescription>
</EmptyHeader>
</Empty>
);
}
return (
<ul className="divide-y rounded-xl border">
{items.map((notification) => (
<li
key={notification.id}
className="flex items-center gap-3 px-4 py-3 text-sm"
>
<span className="flex-1">{notification.title}</span>
{notification.readAt ? null : <Badge>{t("New")}</Badge>}
<span className="text-muted-foreground text-xs tabular-nums">
{format.dateTime(new Date(notification.createdAt), {
dateStyle: "medium",
timeStyle: "short",
})}
</span>
</li>
))}
</ul>
);
}
function NotificationListSkeleton() {
return (
<div className="space-y-2" aria-busy="true">
{Array.from({ length: 4 }, (_, index) => (
<Skeleton key={index} className="h-11 w-full" />
))}
</div>
);
}
Native: React Native
View in the exampleimport { useNotifications } from "better-supabase/blocks/notifications/react";
import { tenantOf, useAuth, useSupabase } from "better-supabase/react";
import { ActivityIndicator, FlatList, Text, View } from "react-native";
import { blocks } from "../lib/blocks";
/**
* The caller's latest notifications, loaded again after each broadcast on
* their private topic `notifications:<user id>`: a new one, or one marked read.
*/
export function NotificationList() {
const auth = useAuth();
const supabase = useSupabase();
const tenant = tenantOf(auth);
const userId = auth.user?.id;
const { items } = useNotifications({
topic: userId ? `notifications:${userId}` : null,
load: async () => {
if (!tenant) return [];
const result = await blocks(supabase).notifications.list({
tenant,
limit: 20,
});
return result.ok ? result.data : [];
},
});
if (items === undefined) return <ActivityIndicator />;
return (
<FlatList
data={items}
keyExtractor={(item) => item.id}
ListEmptyComponent={<Text>Nothing here yet</Text>}
renderItem={({ item }) => (
<View style={{ flexDirection: "row", gap: 8, paddingVertical: 12 }}>
<Text style={{ flex: 1 }}>{item.data.title}</Text>
{item.readAt ? null : <Text style={{ fontWeight: "600" }}>New</Text>}
<Text style={{ color: "#737373" }}>
{item.createdAt.toLocaleString(undefined, {
dateStyle: "medium",
timeStyle: "short",
})}
</Text>
</View>
)}
/>
);
}