# Consent register

> The purposes an organization asks consent for, with their lawful basis, version, how long a grant lasts and whether it needs a signature.

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

## Install the block

```bash
npx better-supabase add consents
```

## Web (Next.js)

Add the shadcn components:

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

From `apps/examples/nextjs/src/features/compliance/components/consent-register.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/compliance/components/consent-register.tsx):

```tsx
// 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 purposes the organization asks consent for,
 * with their lawful basis, version and how long a grant lasts.
 */
export async function ConsentRegister() {
  const [state, t] = await Promise.all([
    getPurposes(),
    getExtracted("compliance"),
  ]);

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

  const basis = (legalBasis: LegalBasis) => {
    switch (legalBasis) {
      case "consent": {
        return t("Consent");
      }

      case "contract": {
        return t("Contract");
      }

      case "legal_obligation": {
        return t("Legal obligation");
      }

      case "vital_interest": {
        return t("Vital interest");
      }

      case "public_task": {
        return t("Public task");
      }

      case "legitimate_interest": {
        return t("Legitimate interest");
      }

      default: {
        const never: never = legalBasis;

        return String(never);
      }
    }
  };

  return (
    <Card data-recipe="consent-register">
      <CardHeader>
        <CardTitle>{t("Consent register")}</CardTitle>
        <CardDescription>
          {t(
            "What the organization asks consent for. A new version of a purpose asks every record again.",
          )}
        </CardDescription>
      </CardHeader>
      <CardContent>
        {state.kind === "error" ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The consent purposes 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.data.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <FileCheckIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No consent purposes")}</EmptyTitle>
              <EmptyDescription>
                {t(
                  "Save a purpose, such as marketing email, before recording consent on a record.",
                )}
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <a
                href="https://bettersupabase.com/docs/blocks/consents"
                className={buttonVariants()}
              >
                {t("Read the consents guide")}
              </a>
            </EmptyContent>
          </Empty>
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("Purpose")}</TableHead>
                <TableHead>{t("Lawful basis")}</TableHead>
                <TableHead>{t("Version")}</TableHead>
                <TableHead>{t("Lasts")}</TableHead>
                <TableHead>{t("Evidence")}</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {state.data.map((row) => (
                <TableRow key={row.id}>
                  <TableCell>
                    <div className="flex flex-col gap-1">
                      <span className="font-medium">{row.title}</span>
                      <span className="text-xs text-muted-foreground">
                        {row.key}
                      </span>
                    </div>
                  </TableCell>
                  <TableCell>{basis(row.legalBasis)}</TableCell>
                  <TableCell className="tabular-nums">{row.version}</TableCell>
                  <TableCell className="text-xs text-muted-foreground">
                    {row.validMonths === null
                      ? t("Until withdrawn")
                      : t("{months, number} months", {
                          months: row.validMonths,
                        })}
                  </TableCell>
                  <TableCell>
                    <div className="flex gap-1">
                      {row.requiresSignature ? (
                        <Badge variant="info-light">{t("Signature")}</Badge>
                      ) : (
                        <Badge variant="secondary">{t("Any")}</Badge>
                      )}
                      {row.system ? (
                        <Badge variant="secondary">{t("System")}</Badge>
                      ) : null}
                    </div>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        )}
      </CardContent>
    </Card>
  );
}

export function ConsentRegisterSkeleton() {
  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 "Consent register" in a React web app that uses shadcn/ui components. The purposes an organization asks consent for, with their lawful basis, version, how long a grant lasts and whether it needs a signature.

It uses the better-supabase `consents` block (Consents). 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 consents` 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/compliance/components/consent-register.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/consent-register.md
- Block docs: https://bettersupabase.com/docs/blocks/consents
```
