# Signing keys rotation

> The active and previous signing keys with a button to rotate them.

Source: https://bettersupabase.com/docs/registry/ui-recipes/signing-keys-rotation

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

It loads its data on the server through the block's client; the [signing-keys block](https://bettersupabase.com/blocks/signing-keys) must be installed (`npx better-supabase add signing-keys`). Its page on bettersupabase.com is [/recipes/signing-keys-rotation](https://bettersupabase.com/recipes/signing-keys-rotation).

Web, with shadcn components:

```bash
npx shadcn add alert badge button card empty skeleton table
```

```tsx title="apps/examples/nextjs/src/features/signing-keys/components/signing-keys-rotation.tsx"
type SigningKeysState =
  | { readonly kind: "ok"; readonly overview: SigningKeysOverview }
  | { readonly kind: "error" }
  | { readonly kind: "no-organization" };

async function loadSigningKeysState(): Promise<SigningKeysState> {
  try {
    const overview = await loadSigningKeys();

    return overview ? { kind: "ok", overview } : { kind: "no-organization" };
  } catch (error) {
    unstable_rethrow(error);

    return { kind: "error" };
  }
}

// oxlint-disable-next-line typescript/require-await -- a Server Action must be async, and refresh() returns nothing to await
async function retry() {
  "use server";
  refresh();
}

/**
 * Render inside `<Suspense>`: the organization's active signing key and the
 * retired ones still published, with a rotate button. The keys are service-role
 * only, so this checks `organization.update` before it reads.
 */
export async function SigningKeysRotation() {
  const [session, t] = await Promise.all([
    getSession(),
    getExtracted("signingKeys"),
  ]);

  if (!can(session, SIGNING_KEYS_PERMISSION)) {
    forbidden();
  }

  const state = await loadSigningKeysState();

  if (state.kind === "no-organization") {
    return null;
  }

  const rows =
    state.kind === "ok"
      ? [
          ...(state.overview.active
            ? [{ ...state.overview.active, active: true }]
            : []),
          ...state.overview.previous.map((key) => ({ ...key, active: false })),
        ]
      : [];

  return (
    <Card data-recipe="signing-keys-rotation">
      <CardHeader>
        <CardTitle>{t("Signing keys")}</CardTitle>
        <CardDescription>
          {t(
            "The keys that sign this organization's tokens. Private keys stay in Vault; receivers check signatures against the published JWKS.",
          )}
        </CardDescription>
        {rows.length > 0 ? (
          <CardAction>
            <RotateSigningKeyButton first={false} />
          </CardAction>
        ) : null}
      </CardHeader>
      <CardContent>
        {state.kind === "error" ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The signing keys didn't load")}</AlertTitle>
            <AlertDescription>{t("Try again in a moment.")}</AlertDescription>
            <AlertAction>
              <form action={retry}>
                <Button size="sm" variant="outline" type="submit">
                  {t("Retry")}
                </Button>
              </form>
            </AlertAction>
          </Alert>
        ) : rows.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <KeyRoundIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No signing key yet")}</EmptyTitle>
              <EmptyDescription>
                {t("Create one before the organization signs any tokens.")}
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <RotateSigningKeyButton first />
            </EmptyContent>
          </Empty>
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("Key ID")}</TableHead>
                <TableHead>{t("Algorithm")}</TableHead>
                <TableHead>{t("Status")}</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {rows.map((key) => (
                <TableRow key={key.kid}>
                  <TableCell className="font-mono text-xs">{key.kid}</TableCell>
                  <TableCell className="text-sm">{key.algorithm}</TableCell>
                  <TableCell>
                    {key.active ? (
                      <Badge variant="success-light">{t("Active")}</Badge>
                    ) : (
                      <Badge variant="secondary">{t("Previous")}</Badge>
                    )}
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        )}
      </CardContent>
    </Card>
  );
}

export function SigningKeysRotationSkeleton() {
  return <Skeleton className="h-40 rounded-xl" aria-busy="true" />;
}
```