# Service principals table

> Machine identities with their scopes, last use and a button to disable one.

- Block: [service-principals](https://bettersupabase.com/blocks/service-principals.md)
- Kind: component
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/service-principals-table

## Install the block

```bash
npx better-supabase add service-principals
```

## Web (Next.js)

Add the shadcn components:

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

From `apps/examples/nextjs/src/features/service-principals/components/service-principals-table.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/service-principals/components/service-principals-table.tsx):

```tsx
type PrincipalsState =
  | { readonly kind: "ok"; readonly principals: readonly ServicePrincipalRow[] }
  | { readonly kind: "error" }
  | { readonly kind: "no-organization" };

async function loadPrincipalsState(): Promise<PrincipalsState> {
  try {
    const principals = await loadServicePrincipals();

    return principals
      ? { kind: "ok", principals }
      : { 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 machine identities workflows and webhooks
 * run as, with their role. Members with `service_principals.manage` add
 * and disable them. Members without `service_principals.read` see nothing.
 */
export async function ServicePrincipalsTable() {
  const [session, t, format] = await Promise.all([
    getSession(),
    getExtracted("servicePrincipals"),
    getFormatter(),
  ]);

  if (!can(session, "service_principals.read")) {
    return null;
  }

  const state = await loadPrincipalsState();

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

  const manage = can(session, "service_principals.manage");

  const roles = new Map([
    ["owner", t("Owner")],
    ["admin", t("Admin")],
    ["member", t("Member")],
  ]);

  return (
    <Card data-recipe="service-principals-table">
      <CardHeader>
        <CardTitle>{t("Service principals")}</CardTitle>
        <CardDescription>
          {t(
            "Machine identities that workflows and webhooks run as. They can't sign in.",
          )}
        </CardDescription>
        {manage && state.kind === "ok" ? (
          <CardAction>
            <CreateServicePrincipalDialog />
          </CardAction>
        ) : null}
      </CardHeader>
      <CardContent>
        {state.kind === "error" ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The service principals 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>
        ) : state.principals.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <BotIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No service principals yet")}</EmptyTitle>
              <EmptyDescription>
                {manage
                  ? t("Add one so automations don't run as a person.")
                  : t("An admin can add one for automations.")}
              </EmptyDescription>
            </EmptyHeader>
            {manage ? (
              <EmptyContent>
                <CreateServicePrincipalDialog />
              </EmptyContent>
            ) : null}
          </Empty>
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("Name")}</TableHead>
                <TableHead>{t("Role")}</TableHead>
                <TableHead>{t("Status")}</TableHead>
                <TableHead>{t("Created")}</TableHead>
                {manage ? (
                  <TableHead className="text-right">{t("Actions")}</TableHead>
                ) : null}
              </TableRow>
            </TableHeader>
            <TableBody>
              {state.principals.map((principal) => (
                <TableRow key={principal.id}>
                  <TableCell>
                    <div className="flex items-center gap-2">
                      <span className="text-sm font-medium">
                        {principal.name}
                      </span>
                      {principal.isDefault ? (
                        <Badge variant="primary-light">{t("Default")}</Badge>
                      ) : null}
                    </div>
                  </TableCell>
                  <TableCell className="text-sm">
                    {roles.get(principal.role) ?? principal.role}
                  </TableCell>
                  <TableCell>
                    {principal.disabledAt ? (
                      <Badge variant="secondary">{t("Disabled")}</Badge>
                    ) : (
                      <Badge variant="success-light">{t("Active")}</Badge>
                    )}
                  </TableCell>
                  <TableCell className="text-xs text-muted-foreground">
                    {format.dateTime(new Date(principal.createdAt), {
                      dateStyle: "medium",
                    })}
                  </TableCell>
                  {manage ? (
                    <TableCell className="text-right">
                      {principal.disabledAt ? null : (
                        <DisableServicePrincipalButton
                          id={principal.id}
                          name={principal.name}
                        />
                      )}
                    </TableCell>
                  ) : null}
                </TableRow>
              ))}
            </TableBody>
          </Table>
        )}
      </CardContent>
    </Card>
  );
}

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

## Prompt

Paste this into a coding agent to build the recipe in your app.

### Web

```text
Build "Service principals table" in a React web app that uses shadcn/ui components. Machine identities with their scopes, last use and a button to disable one.

It uses the better-supabase `service-principals` block (Service principals). It loads its data on the server through the block's client.

Steps:

1. If the app doesn't have the block yet, add the SQL with `npx better-supabase add service-principals` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert badge button card empty skeleton table`.
3. The recipe needs no glue: it calls the block's client on the server.
4. Write the component. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/service-principals/components/service-principals-table.tsx`). Adapt it to the app's existing components, styling and data loading instead of copying it as is.
5. Check the result against the recipe and the block's docs: it typechecks, it reads the same block data, and it handles the loading and empty states the snippet handles.

Read the full code first:

- Recipe: https://bettersupabase.com/recipes/service-principals-table.md
- Block docs: https://bettersupabase.com/docs/blocks/service-principals
```
