# Access requests inbox

> Requests for access to a resource, with approve and deny buttons for the owner.

- Block: [access-requests](https://bettersupabase.com/blocks/access-requests.md)
- Kind: screen
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/access-requests-inbox

## Install the block

```bash
npx better-supabase add access-requests
```

## Web (Next.js)

Add the shadcn components:

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

From `apps/examples/nextjs/src/features/access-requests/components/access-requests-inbox.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/access-requests/components/access-requests-inbox.tsx):

```tsx
/**
 * Requests for a temporary role in the active organization. Members ask
 * for one; members with `members.update_role` approve or deny it, and the
 * role goes back to the previous one when the duration runs out.
 */
export async function AccessRequestsInbox() {
  if (!can(await getSession(), "access_requests.create")) {
    forbidden();
  }

  const [t, format, result] = await Promise.all([
    getExtracted("accessRequests"),
    getFormatter(),
    loadInbox(),
  ]);

  if (result.kind === "error") {
    return (
      <Alert variant="destructive" data-recipe="access-requests-inbox">
        <TriangleAlertIcon />
        <AlertTitle>{t("Access requests could not be loaded")}</AlertTitle>
        <AlertDescription>{t("Try again in a moment.")}</AlertDescription>
        <AlertAction>
          <form action={refreshAccessRequests}>
            <Button size="sm" variant="outline" type="submit">
              {t("Retry")}
            </Button>
          </form>
        </AlertAction>
      </Alert>
    );
  }

  if (result.data === null) {
    return (
      <p
        className="text-sm text-muted-foreground"
        data-recipe="access-requests-inbox"
      >
        {t("Pick an organization to see its access requests.")}
      </p>
    );
  }

  const { requests, role, userId, canDecide } = result.data;
  const canRequest = role !== undefined && role !== "owner";

  const statusLabel = (status: AccessRequestStatus) => {
    switch (status) {
      case "pending": {
        return t("Pending");
      }

      case "granted": {
        return t("Granted");
      }

      case "rejected": {
        return t("Denied");
      }

      case "cancelled": {
        return t("Withdrawn");
      }

      case "expired": {
        return t("Expired");
      }

      case "ended": {
        return t("Ended");
      }

      default: {
        const unknown: never = status;

        return unknown;
      }
    }
  };

  const roleLabel = (value: string) =>
    value === "admin" ? t("Admin") : value === "member" ? t("Member") : value;

  const durationLabel = (value: string | null) => {
    switch (value) {
      case null: {
        return t("Until revoked");
      }

      case "PT1H": {
        return t("1 hour");
      }

      case "P1D": {
        return t("1 day");
      }

      case "P7D": {
        return t("7 days");
      }

      default: {
        return value;
      }
    }
  };

  const date = (value: string) =>
    format.dateTime(new Date(value), {
      dateStyle: "medium",
      timeStyle: "short",
    });

  return (
    <div className="flex flex-col gap-6" data-recipe="access-requests-inbox">
      {canRequest ? (
        <Card id="request-access">
          <CardHeader>
            <CardTitle>{t("Request access")}</CardTitle>
            <CardDescription>
              {t(
                "Ask for a higher role for a limited time. An admin or owner approves it.",
              )}
            </CardDescription>
          </CardHeader>
          <CardContent>
            <RequestAccessForm currentRole={role} />
          </CardContent>
        </Card>
      ) : null}
      <Card>
        <CardHeader>
          <CardTitle>{t("Requests")}</CardTitle>
          <CardDescription>
            {canDecide
              ? t("Approve or deny what members asked for.")
              : t("The requests you sent and what happened to them.")}
          </CardDescription>
        </CardHeader>
        <CardContent>
          {requests.length === 0 ? (
            <Empty>
              <EmptyHeader>
                <EmptyMedia variant="icon">
                  <InboxIcon />
                </EmptyMedia>
                <EmptyTitle>{t("No access requests")}</EmptyTitle>
                <EmptyDescription>
                  {canRequest
                    ? t("Requests you and other members send show up here.")
                    : t(
                        "Requests from members show up here for you to decide.",
                      )}
                </EmptyDescription>
              </EmptyHeader>
              <EmptyContent>
                {canRequest ? (
                  <a
                    href="#request-access"
                    className={buttonVariants({ variant: "outline" })}
                  >
                    {t("Request a role")}
                  </a>
                ) : (
                  <Link
                    href="/settings/members"
                    className={buttonVariants({ variant: "outline" })}
                  >
                    {t("View members")}
                  </Link>
                )}
              </EmptyContent>
            </Empty>
          ) : (
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>{t("Member")}</TableHead>
                  <TableHead>{t("Role")}</TableHead>
                  <TableHead className="hidden md:table-cell">
                    {t("Duration")}
                  </TableHead>
                  <TableHead>{t("Status")}</TableHead>
                  <TableHead className="text-right">
                    <span className="sr-only">{t("Actions")}</span>
                  </TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {requests.map((request) => (
                  <TableRow key={request.id}>
                    <TableCell>
                      <div className="flex flex-col gap-1">
                        <span className="font-medium">
                          {request.userId === userId
                            ? t("You")
                            : request.requester}
                        </span>
                        <span className="text-xs text-muted-foreground">
                          {request.reason ?? date(request.createdAt)}
                        </span>
                      </div>
                    </TableCell>
                    <TableCell>{roleLabel(request.role)}</TableCell>
                    <TableCell className="hidden text-muted-foreground md:table-cell">
                      {request.status === "granted" && request.expiresAt
                        ? t("Until {date}", { date: date(request.expiresAt) })
                        : durationLabel(request.duration)}
                    </TableCell>
                    <TableCell>
                      <Badge variant={STATUS_VARIANT[request.status]}>
                        {statusLabel(request.status)}
                      </Badge>
                    </TableCell>
                    <TableCell className="text-right">
                      <AccessRequestActions
                        request={request}
                        mine={request.userId === userId}
                        canDecide={canDecide}
                      />
                    </TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          )}
        </CardContent>
      </Card>
    </div>
  );
}
```

## Prompt

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

### Web

```text
Build "Access requests inbox" in a React web app that uses shadcn/ui components. Requests for access to a resource, with approve and deny buttons for the owner.

It uses the better-supabase `access-requests` block (Access requests). 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 access-requests` 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 screen. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/access-requests/components/access-requests-inbox.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/access-requests-inbox.md
- Block docs: https://bettersupabase.com/docs/blocks/access-requests
```
