# Connector credentials

> A sheet that stores a connector credential encrypted and revokes it, without reading the secret back.

- Blocks: [connectors](https://bettersupabase.com/blocks/connectors.md), [credentials](https://bettersupabase.com/blocks/credentials.md)
- Kind: component
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/connector-setup

## Install the blocks

```bash
npx better-supabase add connectors credentials
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add button field input sheet
```

From `apps/examples/nextjs/src/features/workflows/builder/components/credential-sheet.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/workflows/builder/components/credential-sheet.tsx):

```tsx
"use client";

import { useAction } from "better-supabase/react";
import { KeySquareIcon, Trash2Icon } from "lucide-react";
import { useExtracted } from "next-intl";
import { useId } from "react";
import { toast } from "sonner";

import { Button } from "@/components/ui/button";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet";
import { useErrorMessage } from "@/lib/use-error-message";

import type { CredentialRow } from "../builder-queries";

import { createCredential, revokeCredential } from "../builder-actions";

/** The organization's Slack credentials: the token goes to Vault, the row keeps a reference. */
export function CredentialSheet({
  credentials,
}: {
  credentials: readonly CredentialRow[];
}) {
  const t = useExtracted("workflows");
  const id = useId();
  const errorMessage = useErrorMessage();

  const onError = (error: Parameters<typeof errorMessage>[0]) => {
    toast.error(errorMessage(error));
  };

  const create = useAction(createCredential, {
    onError,
    onSuccess: () => {
      toast.success(t("Credential saved"));
    },
  });

  const revoke = useAction(revokeCredential, { onError });

  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" size="sm" />}>
        <KeySquareIcon />
        {t("Credentials")}
      </SheetTrigger>
      <SheetContent className="gap-6 p-6">
        <SheetHeader className="p-0">
          <SheetTitle>{t("Credentials")}</SheetTitle>
          <SheetDescription>
            {t(
              "Slack bot tokens for the Post to Slack step. The token is stored in Vault and never shown again.",
            )}
          </SheetDescription>
        </SheetHeader>
        <ul className="space-y-2">
          {credentials.map((credential) => (
            <li
              key={credential.id}
              className="flex items-center justify-between gap-2 text-sm"
            >
              <span className="truncate">{credential.name}</span>
              <Button
                variant="ghost"
                size="sm"
                aria-label={t("Revoke")}
                disabled={revoke.pending}
                onClick={() => {
                  void revoke.run({ id: credential.id });
                }}
              >
                <Trash2Icon />
              </Button>
            </li>
          ))}
        </ul>
        <form
          className="space-y-3"
          action={(form) => {
            void create.run({
              name: String(form.get("name") ?? ""),
              secret: String(form.get("secret") ?? ""),
            });
          }}
        >
          <Field>
            <FieldLabel htmlFor={`${id}-name`}>{t("Name")}</FieldLabel>
            <Input id={`${id}-name`} name="name" required maxLength={100} />
          </Field>
          <Field>
            <FieldLabel htmlFor={`${id}-secret`}>{t("Bot token")}</FieldLabel>
            <Input
              id={`${id}-secret`}
              name="secret"
              type="password"
              required
              autoComplete="off"
              placeholder="xoxb-…"
            />
          </Field>
          <Button type="submit" disabled={create.pending}>
            {t("Add credential")}
          </Button>
        </form>
      </SheetContent>
    </Sheet>
  );
}
```

## Prompt

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

### Web

```text
Build "Connector credentials" in a React web app that uses shadcn/ui components. A sheet that stores a connector credential encrypted and revokes it, without reading the secret back.

It uses the better-supabase `connectors` block (Connectors) with `credentials`. It loads its data on the server through the block's client.

Steps:

1. If the app doesn't have the blocks yet, add the SQL with `npx better-supabase add connectors credentials` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add button field input sheet`.
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/workflows/builder/components/credential-sheet.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/connector-setup.md
- Block docs: https://bettersupabase.com/docs/blocks/connectors
```
