# Singles

Define single-document content like site settings, headers, and footers.

A single is a one-off document — content that exists as exactly one instance. Site settings, navigation headers, footers, and homepage configurations are typical singles. Unlike [collections](https://nextlyhq.com/docs/configuration/collections.md), singles have no list view, no create/delete operations, and are auto-created on first access.

> **Source of truth:** the `SingleConfig` interface lives in `packages/nextly/src/singles/config/types.ts`. The `defineSingle()` helper is in `packages/nextly/src/singles/config/define-single.ts`.

**Code-first**

## Defining a single

Use `defineSingle()` from `nextly`:

```typescript title="src/singles/site-settings.ts"
import {
  defineSingle,
  text,
  upload,
  group,
  repeater,
} from "nextly";

export default defineSingle({
  slug: "site-settings",
  label: { singular: "Site Settings" },
  admin: {
    group: "Settings",
    icon: "Settings",
    description: "Global site configuration",
  },
  fields: [
    text({ name: "siteName", required: true, label: "Site Name" }),
    text({ name: "tagline", label: "Tagline" }),
    upload({ name: "logo", relationTo: "media", label: "Logo" }),
    upload({ name: "favicon", relationTo: "media", label: "Favicon" }),
    group({
      name: "seo",
      label: "SEO Defaults",
      fields: [
        text({ name: "metaTitle", label: "Default Meta Title" }),
        text({ name: "metaDescription", label: "Default Meta Description" }),
      ],
    }),
    repeater({
      name: "socialLinks",
      label: "Social Links",
      fields: [
        text({ name: "platform", required: true }),
        text({ name: "url", required: true }),
      ],
    }),
  ],
  access: {
    read: true,
    update: ({ permissions }) => permissions.includes("site-settings:update"),
  },
  hooks: {
    afterChange: [
      async ({ data }) => {
        // Nextly invalidates its own cache tags on every write once the cache
        // revalidator is registered (see ISR caching). Use hooks for side
        // effects of your own.
        console.info("site settings updated", data);
      },
    ],
  },
});
```

Then register it in your config:

```typescript title="nextly.config.ts"
import { defineConfig } from "nextly";
import SiteSettings from "./src/singles/site-settings";
import Header from "./src/singles/header";
import Footer from "./src/singles/footer";

export default defineConfig({
  singles: [SiteSettings, Header, Footer],
});
```

**Visual Schema Builder**

## Creating a single in the Visual Schema Builder

The Nextly admin panel ships with a Visual Schema Builder for creating singles without writing code. (We'll just call it the Schema Builder below.)

1. Open the admin panel and navigate to **Builder > Singles**.
2. Click **Create Single**.
3. Enter a slug (e.g. `site-settings`) and a display label.
4. Add fields using the drag-and-drop field editor.
5. Configure access control and hooks through the UI.
6. Click **Save**.

The Schema Builder produces the same `SingleConfig` that code-first singles use. Unlike a collection, a builder-created single has no command that moves it into code.

## How singles differ from collections

|                               | Collections                             | Singles                                                            |
| ----------------------------- | --------------------------------------- | ------------------------------------------------------------------ |
| **Entries**                   | Many                                    | Exactly one                                                        |
| **List view**                 | Yes                                     | No                                                                 |
| **Create/Delete**             | Yes                                     | No (auto-created on first access; cannot be deleted)               |
| **Access control operations** | `create`, `read`, `update`, `delete`    | `read`, `update` only                                              |
| **Hooks**                     | 8 hooks                                 | 4 hooks (`beforeRead`, `afterRead`, `beforeChange`, `afterChange`) |
| **Default table prefix**      | None — uses slug as table name          | `single_` (e.g. `single_site_settings`)                            |
| **API endpoint**              | `/admin/api/collections/[slug]/entries` | `/admin/api/singles/[slug]`                                        |

## Single options

Only `slug` and `fields` are required.

### `slug`

|              |          |
| ------------ | -------- |
| **Type**     | `string` |
| **Required** | Yes      |

Unique identifier across all singles **and** collections **and** field groups. Used as the API endpoint path and database table name (with the `single_` prefix unless `dbName` is set). Must be lowercase and URL-friendly.

### `fields`

|              |                 |
| ------------ | --------------- |
| **Type**     | `FieldConfig[]` |
| **Required** | Yes             |

Array of field definitions. Singles support the same field types as collections. See [Fields](https://nextlyhq.com/docs/configuration/fields.md).

### `label`

|             |                          |
| ----------- | ------------------------ |
| **Type**    | `{ singular: string }`   |
| **Default** | Auto-generated from slug |

Display label in the admin sidebar, breadcrumbs, and page titles. Singles only need a singular label since there is exactly one document.

### `dbName`

|             |                                               |
| ----------- | --------------------------------------------- |
| **Type**    | `string`                                      |
| **Default** | `single_[slug]` (e.g. `single_site_settings`) |

Custom database table name.

### `description`

|             |             |
| ----------- | ----------- |
| **Type**    | `string`    |
| **Default** | `undefined` |

Description displayed in the admin UI. Falls back to `admin.description`.

### `sanitize`

|             |           |
| ----------- | --------- |
| **Type**    | `boolean` |
| **Default** | `true`    |

When enabled, the global sanitization hook strips HTML tags from plain-text fields (`text`, `textarea`, `email`) before storage. Set to `false` only if the single intentionally stores HTML in those fields.

### `custom`

|             |                           |
| ----------- | ------------------------- |
| **Type**    | `Record<string, unknown>` |
| **Default** | `undefined`               |

Arbitrary metadata for hooks, plugins, or custom code. Not persisted to the database.

## Admin options

Configure how the single appears in the admin panel via the `admin` property.

| Property       | Type      | Default | Description                                                                            |
| -------------- | --------- | ------- | -------------------------------------------------------------------------------------- |
| `group`        | `string`  | None    | Sidebar group name.                                                                    |
| `icon`         | `string`  | None    | Lucide icon name (e.g. `"Settings"`, `"Menu"`, `"Home"`).                              |
| `hidden`       | `boolean` | `false` | Hide from sidebar navigation (still reachable via direct URL and API).                 |
| `order`        | `number`  | `100`   | Sort order within sidebar group (lower = higher).                                      |
| `sidebarGroup` | `string`  | None    | Custom sidebar group slug; moves the entry from its default section to a custom group. |
| `description`  | `string`  | None    | Help text below the single title.                                                      |

> **Singles do not support `useAsTitle`, `pagination`, `preview`, or admin-component overrides** — those concepts are collection-specific.

## Access control

Singles only support `read` and `update` — there is no create or delete since the document is auto-created on first access and cannot be removed.

```typescript
access: {
  read: true,
  update: ({ permissions, collection }) =>
    permissions.includes(`${collection}:update`),
}
```

Each operation accepts a function (returning `boolean | Promise<boolean>`), a literal `boolean`, or can be omitted to fall back to database role/permission checks. A declared rule is what a signed-in user is judged by, so it checks the permission itself and narrows from there; don't decide by a role's name. Super-admin always bypasses checks. The `AccessControlContext` shape is the same as for [collections](https://nextlyhq.com/docs/configuration/collections.md#access-control).

## Hooks

Singles support four lifecycle hooks — a subset of the eight available on collections.

### Five available hooks

| Hook             | Triggers on                                          | Can modify             |
| ---------------- | ---------------------------------------------------- | ---------------------- |
| `beforeRead`     | Before reading from the database.                    | Yes (query parameters) |
| `afterRead`      | After reading from the database.                     | Yes (transform output) |
| `beforeValidate` | Before the schema's rules are enforced on an update. | Yes (data)             |
| `beforeChange`   | After validation passes, before the database write.  | Yes (data)             |
| `afterChange`    | After the database write.                            | No (side effects)      |

### Execution order — read

1. `beforeRead`
2. **Database read**
3. `afterRead`

### Execution order — update

1. `beforeValidate`
2. **Validation** - the schema's declared rules are enforced
3. `beforeChange`
4. **Database update**
5. `afterChange`

The validation gate is what separates the two write hooks. Use `beforeValidate`
to supply or repair a value you want the rules applied to; use `beforeChange` to
derive the value that gets stored, knowing the document has already passed them.
What a `beforeChange` handler returns is written without being re-validated.

### Example: cache invalidation

```typescript title="src/singles/site-settings.ts"
hooks: {
  afterChange: [
    async ({ data }) => {
      // Nextly's own cache tags are already invalidated on every write once
      // the cache revalidator is registered (see ISR caching). Notify a
      // system of your own here, with an absolute URL: a relative fetch
      // throws on the server.
      await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/my-webhook`, {
        method: "POST",
        body: JSON.stringify(data),
      });
    },
  ],
  afterRead: [
    async ({ data }) => {
      // Add a computed field
      return { ...data, fullTitle: `${data.siteName} - ${data.tagline}` };
    },
  ],
}
```

### When to use which hook

* `beforeRead` — modify query filters (e.g., scope to the current tenant), audit logging.
* `afterRead` — compute virtual properties, redact sensitive fields.
* `beforeValidate` — coerce inputs (trim strings, normalize casing), or supply a value the rules require.
* `beforeChange` — final mutations before the row is persisted, on data already known to be valid.
* `afterChange` — cache busting, webhooks, queue jobs.

Throwing inside any hook aborts the operation and rolls back the transaction.

## Example: header navigation

```typescript title="src/singles/header.ts"
import { defineSingle, repeater, text, relationship } from "nextly";

export default defineSingle({
  slug: "header",
  label: { singular: "Header Navigation" },
  admin: {
    group: "Navigation",
    icon: "Menu",
  },
  fields: [
    repeater({
      name: "navItems",
      label: "Navigation Items",
      fields: [
        text({ name: "label", required: true }),
        text({ name: "url" }),
        relationship({ name: "page", relationTo: "pages" }),
      ],
    }),
  ],
  access: {
    read: true,
    update: ({ permissions }) => permissions.includes("header:update"),
  },
});
```

## Next steps

* [Collections](https://nextlyhq.com/docs/configuration/collections.md) — content types with multiple entries
* [Fields](https://nextlyhq.com/docs/configuration/fields.md) — every field type and validation option
* [Field Groups](https://nextlyhq.com/docs/configuration/field-groups.md) — reusable field groups for collections and singles
* [Visual Schema Builder](https://nextlyhq.com/docs/schema-builder.md) — create singles visually with drag-and-drop
