Sessions list
The caller's signed-in devices with the last activity and a button to end each session. It loads its data on the server through the block's client.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add alert-dialog badge button cardInstall the glue
Install the block with
npx better-supabase add sessions. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the component
React and shadcn
View in the examplesessions-list.tsx /** * The caller's signed-in session with its sign-in method and time, and the * sessions on other devices (`list_my_sessions`). Each one can be ended on * its own (`end_my_session`), or all of them at once * (`signOut({ scope: "others" })`). */ export function SessionsList({ session, others, }: { session: CurrentSession; others: readonly OtherSession[]; }) { const t = useExtracted("sessions"); const format = useFormatter(); const supabase = useSupabase(); const router = useRouter(); const changeSession = useSessionChange(sessionChanged, router); const [othersOpen, setOthersOpen] = useState(false); const [endingOthers, startEndingOthers] = useTransition(); const [endingThis, startEndingThis] = useTransition(); const methodLabel = (method: string | null) => { switch (method) { case "password": { return t("Password"); } case "otp": case "magiclink": { return t("Email link"); } case "oauth": { return t("Social sign-in"); } case "sso/saml": { return t("Single sign-on"); } case "totp": { return t("Authenticator app"); } case null: { return t("Unknown method"); } default: { return method; } } }; const signedIn = session.signedInAt ? format.dateTime(new Date(session.signedInAt), { dateStyle: "medium", timeStyle: "short", }) : null; const endOthers = () => { startEndingOthers(async () => { const { error } = await supabase.auth.signOut({ scope: "others" }); if (error) { toast.error(t("Signing out failed. Try again.")); return; } setOthersOpen(false); toast.success(t("Signed out of your other devices")); }); }; const endThis = () => { startEndingThis(async () => { const { error } = await supabase.auth.signOut({ scope: "local" }); if (error) { toast.error(t("Signing out failed. Try again.")); return; } await changeSession("/login"); }); }; return ( <Card data-recipe="sessions-list"> <CardHeader> <CardTitle>{t("Sessions")}</CardTitle> <CardDescription> {t( "The devices you are signed in on. End a session if you lost the device or signed in somewhere you no longer use.", )} </CardDescription> </CardHeader> <CardContent className="flex flex-col gap-4"> <div className="flex flex-wrap items-center gap-3 rounded-lg border p-3"> <MonitorSmartphoneIcon aria-hidden="true" className="size-5 text-muted-foreground" /> <div className="flex min-w-0 flex-1 flex-col gap-1"> <div className="flex flex-wrap items-center gap-2"> <span className="text-sm font-medium">{t("This device")}</span> <Badge variant="success-light">{t("Active now")}</Badge> {session.aal === "aal2" ? ( <Badge variant="info-light">{t("Two-factor")}</Badge> ) : null} </div> <span className="text-xs text-muted-foreground"> {signedIn ? t("{method}, signed in {date}", { date: signedIn, method: methodLabel(session.method), }) : methodLabel(session.method)} </span> </div> <Button type="button" variant="outline" size="sm" disabled={endingThis} onClick={endThis} > <LogOutIcon /> {t("Sign out")} </Button> </div> {others.length === 0 ? ( <p className="text-sm text-muted-foreground"> {t("You are not signed in on any other device.")} </p> ) : ( <ul className="flex flex-col gap-2"> {others.map((other) => ( <OtherSessionRow key={other.id} session={other} /> ))} </ul> )} {others.length > 1 ? ( <div className="flex flex-wrap items-center justify-between gap-2"> <p className="text-sm text-muted-foreground"> {t("Other devices stay signed in until you end their sessions.")} </p> <AlertDialog open={othersOpen} onOpenChange={(open) => { if (!endingOthers) { setOthersOpen(open); } }} > <AlertDialogTrigger render={<Button type="button" variant="outline" size="sm" />} > {t("Sign out other devices")} </AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle> {t("Sign out of your other devices?")} </AlertDialogTitle> <AlertDialogDescription> {t( "Every session except this one ends. Those devices have to sign in again.", )} </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel disabled={endingOthers}> {t("Cancel")} </AlertDialogCancel> <AlertDialogAction type="button" variant="destructive" disabled={endingOthers} onClick={endOthers} > {t("Sign out others")} </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> </div> ) : null} </CardContent> </Card> ); } function OtherSessionRow({ session }: { session: OtherSession }) { const t = useExtracted("sessions"); const format = useFormatter(); const errorMessage = useErrorMessage(); const end = useAction(endSession, { onSuccess: () => { toast.success(t("Session ended")); }, onError: (error) => { toast.error(errorMessage(error)); }, }); const lastActive = format.dateTime(new Date(session.lastActiveAt), { dateStyle: "medium", timeStyle: "short", }); return ( <li className="flex flex-wrap items-center gap-3 rounded-lg border p-3"> <MonitorIcon aria-hidden="true" className="size-5 text-muted-foreground" /> <div className="flex min-w-0 flex-1 flex-col gap-1"> <span className="truncate text-sm font-medium"> {session.userAgent ?? t("Unknown device")} </span> <span className="text-xs text-muted-foreground"> {session.ip ? t("Last active {date} from {ip}", { date: lastActive, ip: session.ip, }) : t("Last active {date}", { date: lastActive })} </span> </div> <Button type="button" variant="outline" size="sm" disabled={end.pending} onClick={() => { void end.run({ id: session.id }); }} > {t("End session")} </Button> </li> ); }