# Custom fields editor

> A settings page that adds, orders and removes the custom fields of a record type.

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

## Install the block

```bash
npx better-supabase add custom-fields
```

## Web (Next.js)

Add the shadcn components:

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

From `apps/examples/nextjs/src/features/custom-fields/components/custom-fields-editor.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/custom-fields/components/custom-fields-editor.tsx):

```tsx
/**
 * Render inside `<Suspense>`: the custom fields of the record type in
 * `?entity=`, in display order. Managers add, relabel, reorder and delete
 * them.
 */
export async function CustomFieldsEditor({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const [session, t, params] = await Promise.all([
    getSession(),
    getExtracted("custom-fields"),
    searchParams,
  ]);

  if (!can(session, "custom_fields.read")) {
    forbidden();
  }

  const parsed = v.safeParse(FieldEntitySchema, params["entity"]);
  const entity = parsed.success ? parsed.output : "customer";
  const fields = await getFields(entity);

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

  const canManage = can(session, "custom_fields.manage");

  return (
    <Card data-recipe="custom-fields-editor">
      <CardHeader>
        <CardTitle>{t("Custom fields")}</CardTitle>
        <CardDescription>
          {t("Extra fields on your records, shown in this order.")}
        </CardDescription>
        <CardAction>
          <div className="flex items-center gap-2">
            <EntityPicker entity={entity} />
            {canManage && fields.kind === "ok" && fields.data.length > 0 ? (
              <FieldDialog key={entity} entity={entity} />
            ) : null}
          </div>
        </CardAction>
      </CardHeader>
      <CardContent>
        {fields.kind === "error" ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The fields didn't load")}</AlertTitle>
            <AlertDescription>{t("Try again in a moment.")}</AlertDescription>
            <AlertAction>
              <form action={retryCustomFields}>
                <Button size="sm" variant="outline" type="submit">
                  {t("Retry")}
                </Button>
              </form>
            </AlertAction>
          </Alert>
        ) : fields.data.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <ListPlusIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No custom fields yet")}</EmptyTitle>
              <EmptyDescription>
                {canManage
                  ? t(
                      "Add a field to record what your team tracks on top of the built-in ones.",
                    )
                  : t("An admin can add fields for this record type.")}
              </EmptyDescription>
            </EmptyHeader>
            {canManage ? (
              <EmptyContent>
                <FieldDialog key={entity} entity={entity} />
              </EmptyContent>
            ) : null}
          </Empty>
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("Label")}</TableHead>
                <TableHead>{t("Key")}</TableHead>
                <TableHead>{t("Type")}</TableHead>
                {canManage ? (
                  <TableHead className="text-right">{t("Actions")}</TableHead>
                ) : null}
              </TableRow>
            </TableHeader>
            <TableBody>
              {fields.data.map((field, index) => (
                <TableRow key={field.id}>
                  <TableCell>
                    <div className="flex items-center gap-2">
                      <span className="font-medium">{field.label}</span>
                      {field.sensitive ? (
                        <Badge variant="warning-light">{t("Sensitive")}</Badge>
                      ) : null}
                    </div>
                  </TableCell>
                  <TableCell className="font-mono text-xs text-muted-foreground">
                    {field.key}
                  </TableCell>
                  <TableCell>
                    <FieldTypeBadge type={field.type} />
                  </TableCell>
                  {canManage ? (
                    <TableCell>
                      <FieldRowControls
                        entity={entity}
                        field={field}
                        first={index === 0}
                        last={index === fields.data.length - 1}
                      />
                    </TableCell>
                  ) : null}
                </TableRow>
              ))}
            </TableBody>
          </Table>
        )}
      </CardContent>
    </Card>
  );
}
```

## Prompt

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

### Web

```text
Build "Custom fields editor" in a React web app that uses shadcn/ui components. A settings page that adds, orders and removes the custom fields of a record type.

It uses the better-supabase `custom-fields` block (Custom fields). 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 custom-fields` 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/custom-fields/components/custom-fields-editor.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/custom-fields-editor.md
- Block docs: https://bettersupabase.com/docs/blocks/custom-fields
```
