# Fields

Complete reference for every field type, their options, and usage patterns.

Fields define the shape of your content. Every [collection](https://nextlyhq.com/docs/configuration/collections.md), [single](https://nextlyhq.com/docs/configuration/singles.md), and [field group](https://nextlyhq.com/docs/configuration/field-groups.md) is composed of fields.

Nextly exports field-helper functions from `nextly`. They eliminate the need to set the `type` property manually and give you full TypeScript autocomplete.

```typescript
import { text, number, select, option } from "nextly";

const fields = [
  text({ name: "title", required: true }),
  number({ name: "price", min: 0 }),
  select({
    name: "priority",
    options: [option("Low"), option("Medium"), option("High")],
  }),
];
```

> **Status / Draft-Published is not a `select` field.** If you want a Draft / Published lifecycle on a collection, use the built-in `status: true` flag on `defineCollection` (or the Advanced-tab toggle in the Schema Builder) -- not a hand-rolled `select` field. See [Draft / Published status](https://nextlyhq.com/docs/guides/draft-published-status.md).

> **Source of truth:** `packages/nextly/src/collections/fields/types/` (one file per field type) and `packages/nextly/src/collections/fields/helpers.ts` (the helper exports).

## Field categories

Seven categories partition the field inventory.

| Category                      | Fields                                                                                    |
| ----------------------------- | ----------------------------------------------------------------------------------------- |
| [Basic](#basic)               | `text`, `textarea`, `richText`, `email`, `password`, `code`, `number`, `checkbox`, `date` |
| [Selection](#selection)       | `select`, `radio`, `chips`                                                                |
| [Media](#media)               | `upload`                                                                                  |
| [Relationship](#relationship) | `relationship`                                                                            |
| [Layout](#layout)             | `repeater`, `group`                                                                       |
| [Component](#component-1)     | `component`                                                                               |
| [Advanced](#advanced)         | `json`                                                                                    |

The "Common options across fields" section at the bottom lists the shared properties (name, label, required, admin, access, hooks, validate, custom).

***

## Basic

### text

Single-line text input. Supports `hasMany` mode for storing arrays of strings (tag-style input).

```typescript
text({ name: "title", required: true })
```

| Option               | Type                                | Default | Description                                                  |
| -------------------- | ----------------------------------- | ------- | ------------------------------------------------------------ |
| `name`               | `string`                            | —       | **Required.** Unique field identifier. Lowercase, no spaces. |
| `minLength`          | `number`                            | —       | Minimum string length.                                       |
| `maxLength`          | `number`                            | —       | Maximum string length. Also sets the DB column size.         |
| `hasMany`            | `boolean`                           | `false` | Accept an array of strings.                                  |
| `minRows`            | `number`                            | —       | Min items when `hasMany: true`.                              |
| `maxRows`            | `number`                            | —       | Max items when `hasMany: true`.                              |
| `defaultValue`       | `string \| string[] \| (data) => …` | —       | Static default or function.                                  |
| `admin.autoComplete` | `string`                            | —       | HTML `autocomplete` attribute (e.g. `"name"`, `"tel"`).      |

```typescript
text({ name: "title", required: true, maxLength: 200 });

// Multiple values (tags)
text({
  name: "tags",
  hasMany: true,
  minRows: 1,
  maxRows: 10,
});
```

**Gotchas:** `maxLength` is the database column width — pick it once and migrate carefully if you change it.

***

### textarea

Multi-line text input.

```typescript
textarea({ name: "description", maxLength: 1000 })
```

| Option         | Type                                             | Default      | Description                  |
| -------------- | ------------------------------------------------ | ------------ | ---------------------------- |
| `minLength`    | `number`                                         | —            | Minimum string length.       |
| `maxLength`    | `number`                                         | —            | Maximum string length.       |
| `defaultValue` | `string \| (data) => string`                     | —            | Static default or function.  |
| `admin.rows`   | `number`                                         | `3`          | Number of visible text rows. |
| `admin.resize` | `"vertical" \| "horizontal" \| "both" \| "none"` | `"vertical"` | Resize behavior.             |

```typescript
textarea({
  name: "bio",
  admin: { rows: 5, resize: "none" },
});
```

***

### richText

Full-featured WYSIWYG editor powered by Lexical. Content is stored as a Lexical editor-state JSON object.

```typescript
richText({ name: "content" })
```

| Option              | Type                                       | Default                      | Description                                              |
| ------------------- | ------------------------------------------ | ---------------------------- | -------------------------------------------------------- |
| `features`          | `RichTextFeature[]`                        | See "Default features" below | Enabled editor features. Pass `[]` for plain text.       |
| `defaultValue`      | `RichTextValue \| (data) => RichTextValue` | —                            | Static default or function.                              |
| `admin.hideToolbar` | `boolean`                                  | `false`                      | Hide the editor toolbar (keyboard shortcuts still work). |

**Default features** when `features` is omitted: `bold`, `italic`, `underline`, `strikethrough`, `code`, `h1`–`h4`, `orderedList`, `unorderedList`, `indent`, `blockquote`, `link`.

**All available features** (`RichTextFeature` union):

| Category        | Features                                                                           |
| --------------- | ---------------------------------------------------------------------------------- |
| Formatting      | `bold`, `italic`, `underline`, `strikethrough`, `code`, `subscript`, `superscript` |
| Text styling    | `fontFamily`, `fontSize`, `fontColor`, `bgColor`                                   |
| Headings        | `h1`, `h2`, `h3`, `h4`, `h5`, `h6`                                                 |
| Lists           | `orderedList`, `unorderedList`, `checkList`, `indent`                              |
| Links and media | `link`, `upload`, `relationship`                                                   |
| Advanced        | `table`, `horizontalRule`, `codeBlock`, `align`                                    |
| Rich media      | `video`, `buttonLink`, `collapsible`, `gallery`                                    |

```typescript
// Simple blog editor
richText({
  name: "content",
  features: ["bold", "italic", "link", "h2", "h3", "orderedList", "unorderedList"],
});

// Full-featured editor
richText({
  name: "body",
  features: [
    "bold", "italic", "underline", "strikethrough", "code",
    "h1", "h2", "h3", "blockquote",
    "orderedList", "unorderedList", "checkList",
    "link", "upload", "table", "codeBlock",
  ],
});
```

**Gotchas:** the value is a JSON object (`{ root: { children: […] } }`) — not an HTML string. Use the REST endpoint's `?richTextFormat=html` query param if you want pre-rendered HTML.

***

### email

Specialised text input with built-in email-format validation.

```typescript
email({ name: "email", required: true, unique: true })
```

| Option               | Type                         | Default   | Description                    |
| -------------------- | ---------------------------- | --------- | ------------------------------ |
| `defaultValue`       | `string \| (data) => string` | —         | Static default or function.    |
| `admin.autoComplete` | `string`                     | `"email"` | HTML `autocomplete` attribute. |

Custom `validate` runs **in addition to** the built-in email-format check, so you can layer on domain restrictions without losing format validation.

```typescript
email({
  name: "workEmail",
  validate: (value) =>
    !value || value.endsWith("@company.com")
      ? true
      : "Must be a company email address",
});
```

***

### password

Masked text input for passwords. Values should be hashed before storage using a `beforeChange` hook, and `read` access should return `false` so hashed values don't escape the database.

```typescript
password({
  name: "password",
  required: true,
  minLength: 8,
})
```

| Option                        | Type                                            | Default          | Description                                                                                   |
| ----------------------------- | ----------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------- |
| `minLength`                   | `number`                                        | —                | Minimum password length. (Helper-level default is none; documented recommended minimum is 8.) |
| `maxLength`                   | `number`                                        | —                | Maximum password length. Most hashing algorithms cap around 72–128 bytes.                     |
| `defaultValue`                | `string \| (data) => string`                    | —                | Static default. Setting one is rarely a good idea security-wise.                              |
| `admin.autoComplete`          | `"new-password" \| "current-password" \| "off"` | `"new-password"` | HTML `autocomplete` attribute.                                                                |
| `admin.showStrengthIndicator` | `boolean`                                       | `false`          | Show a visual password-strength meter.                                                        |

Full working example:

```typescript
password({
  name: "password",
  required: true,
  minLength: 8,
  access: { read: () => false },
  hooks: {
    beforeChange: [
      async ({ value }) => (value ? await bcrypt.hash(value, 10) : value),
    ],
  },
});
```

**Gotchas:** there is no built-in hashing — the field type stores whatever you give it. Always pair with a hashing hook and a `read: () => false` field-level access rule.

***

### code

Code editor with syntax highlighting. Supports 29 languages.

```typescript
code({
  name: "snippet",
  admin: { language: "javascript" },
})
```

| Option                                  | Type                         | Default       | Description                     |
| --------------------------------------- | ---------------------------- | ------------- | ------------------------------- |
| `defaultValue`                          | `string \| (data) => string` | —             | Static default or function.     |
| `admin.language`                        | `CodeLanguage`               | `"plaintext"` | Syntax-highlighting language.   |
| `admin.editorOptions.lineNumbers`       | `boolean`                    | `true`        | Show line numbers.              |
| `admin.editorOptions.wordWrap`          | `boolean`                    | `false`       | Enable word wrapping.           |
| `admin.editorOptions.tabSize`           | `number`                     | `2`           | Tab width in spaces.            |
| `admin.editorOptions.useTabs`           | `boolean`                    | `false`       | Use tabs instead of spaces.     |
| `admin.editorOptions.minHeight`         | `number`                     | `200`         | Minimum editor height in px.    |
| `admin.editorOptions.maxHeight`         | `number`                     | —             | Maximum editor height in px.    |
| `admin.editorOptions.fontSize`          | `number`                     | `14`          | Font size in px.                |
| `admin.editorOptions.fontFamily`        | `string`                     | `"monospace"` | Editor font family.             |
| `admin.editorOptions.folding`           | `boolean`                    | `true`        | Enable code folding.            |
| `admin.editorOptions.matchBrackets`     | `boolean`                    | `true`        | Highlight matching brackets.    |
| `admin.editorOptions.autoCloseBrackets` | `boolean`                    | `true`        | Auto-close brackets and quotes. |

**Supported `CodeLanguage` values:** `javascript`, `typescript`, `jsx`, `tsx`, `html`, `css`, `scss`, `less`, `json`, `markdown`, `yaml`, `xml`, `sql`, `graphql`, `python`, `ruby`, `php`, `java`, `c`, `cpp`, `csharp`, `go`, `rust`, `swift`, `kotlin`, `shell`, `bash`, `powershell`, `dockerfile`, `plaintext`.

```typescript
code({
  name: "snippet",
  admin: {
    language: "javascript",
    editorOptions: { lineNumbers: true, minHeight: 300 },
  },
});
```

***

### number

Numeric input for integers or decimals. Supports `hasMany` mode for arrays of numbers.

```typescript
number({ name: "price", required: true, min: 0, admin: { step: 0.01 } })
```

| Option              | Type                                | Default | Description                         |
| ------------------- | ----------------------------------- | ------- | ----------------------------------- |
| `min`               | `number`                            | —       | Minimum allowed value.              |
| `max`               | `number`                            | —       | Maximum allowed value.              |
| `hasMany`           | `boolean`                           | `false` | Accept an array of numbers.         |
| `minRows`           | `number`                            | —       | Min items when `hasMany: true`.     |
| `maxRows`           | `number`                            | —       | Max items when `hasMany: true`.     |
| `defaultValue`      | `number \| number[] \| (data) => …` | —       | Static default or function.         |
| `admin.step`        | `number`                            | `1`     | Increment step for spinner buttons. |
| `admin.placeholder` | `string`                            | —       | Placeholder text.                   |

```typescript
number({ name: "rating", min: 1, max: 5, admin: { step: 1 } });
```

***

### checkbox

Boolean true/false toggle.

```typescript
checkbox({ name: "published", defaultValue: false })
```

| Option         | Type                           | Default | Description                                                        |
| -------------- | ------------------------------ | ------- | ------------------------------------------------------------------ |
| `defaultValue` | `boolean \| (data) => boolean` | —       | Initial value. If `required: true` and unset, defaults to `false`. |

```typescript
checkbox({
  name: "featured",
  label: "Featured Post",
  admin: { position: "sidebar" },
});

// Required terms-acceptance
checkbox({
  name: "termsAccepted",
  required: true,
  validate: (value) =>
    value === true ? true : "You must accept the terms to continue",
});
```

***

### date

Date and/or time picker. Dates are stored in UTC as ISO 8601 strings.

```typescript
date({ name: "publishedAt" })
```

| Option                        | Type                                                     | Default     | Description                                           |
| ----------------------------- | -------------------------------------------------------- | ----------- | ----------------------------------------------------- |
| `defaultValue`                | `string \| Date \| (data) => string \| Date`             | —           | Static default or function.                           |
| `admin.date.pickerAppearance` | `"dayOnly" \| "dayAndTime" \| "timeOnly" \| "monthOnly"` | `"dayOnly"` | Picker mode.                                          |
| `admin.date.displayFormat`    | `string`                                                 | —           | Display format string (date-fns format).              |
| `admin.date.monthsToShow`     | `1 \| 2`                                                 | `1`         | Months visible in the picker (max 2).                 |
| `admin.date.minDate`          | `Date \| string`                                         | —           | Earliest selectable date.                             |
| `admin.date.maxDate`          | `Date \| string`                                         | —           | Latest selectable date.                               |
| `admin.date.minTime`          | `Date \| string`                                         | —           | Earliest selectable time (when picker includes time). |
| `admin.date.maxTime`          | `Date \| string`                                         | —           | Latest selectable time.                               |
| `admin.date.timeIntervals`    | `number`                                                 | `30`        | Time step in minutes.                                 |
| `admin.date.timeFormat`       | `string`                                                 | `"h:mm aa"` | Time display format.                                  |

```typescript
// Date and time
date({
  name: "eventStart",
  admin: {
    date: { pickerAppearance: "dayAndTime", timeIntervals: 15 },
  },
});

// Time only
date({
  name: "openingTime",
  admin: {
    date: { pickerAppearance: "timeOnly", timeFormat: "HH:mm" },
  },
});

// Month only (e.g., card expiration)
date({
  name: "expirationMonth",
  admin: {
    date: { pickerAppearance: "monthOnly", displayFormat: "MM/yyyy" },
  },
});
```

**Gotchas:** values are stored in UTC; format display in the client when displaying to users.

***

## Selection

### select

Dropdown for choosing from predefined options. Supports single or multi-select with searchable input.

```typescript
import { select, option } from "nextly";

select({
  name: "priority",
  options: [option("Low"), option("Medium"), option("High")],
})
```

> Use `select` for arbitrary categorical options (priority, region, theme, etc.). For a collection's Draft / Published lifecycle, prefer the built-in `status: true` flag on `defineCollection` -- see [Draft / Published status](https://nextlyhq.com/docs/guides/draft-published-status.md).

| Option              | Type                                | Default | Description                                                       |
| ------------------- | ----------------------------------- | ------- | ----------------------------------------------------------------- |
| `options`           | `SelectOption[]`                    | —       | **Required.** Array of `{ label, value }` objects.                |
| `hasMany`           | `boolean`                           | `false` | Allow multiple selections.                                        |
| `enumName`          | `string`                            | Auto    | Custom SQL enum name.                                             |
| `interfaceName`     | `string`                            | —       | TypeScript interface name for code generation.                    |
| `filterOptions`     | `(args) => SelectOption[]`          | —       | Dynamically filter options based on data, sibling data, and user. |
| `defaultValue`      | `string \| string[] \| (data) => …` | —       | Default value(s).                                                 |
| `admin.isClearable` | `boolean`                           | `false` | Show a clear button.                                              |
| `admin.isSortable`  | `boolean`                           | `false` | Enable drag-and-drop reorder (multi-select only).                 |

The `option()` helper auto-generates the value from the label by lowercasing it and replacing spaces with underscores. Pass an explicit second arg if you want a different value.

```typescript
option("Draft");        // { label: "Draft", value: "draft" }
option("In Review");    // { label: "In Review", value: "in_review" }
option("Active", "active");
```

```typescript
// Multi-select
select({
  name: "categories",
  hasMany: true,
  options: [option("Tech"), option("Business"), option("Design")],
  admin: { isClearable: true, isSortable: true },
});

// Cascading dropdown
select({
  name: "subcategory",
  options: allSubcategories,
  filterOptions: ({ data }) =>
    allSubcategories.filter((s) => s.parentId === data.category),
});
```

**Gotchas:** option `value` strings should not contain hyphens or special characters — that's a GraphQL enum constraint. Underscores are fine.

***

### radio

Radio button group for single selection. Best when all options should be visible at once.

```typescript
radio({
  name: "priority",
  options: [option("Low"), option("Medium"), option("High")],
})
```

| Option          | Type                         | Default        | Description                                        |
| --------------- | ---------------------------- | -------------- | -------------------------------------------------- |
| `options`       | `SelectOption[]`             | —              | **Required.** Array of `{ label, value }` objects. |
| `enumName`      | `string`                     | Auto           | Custom SQL enum name.                              |
| `interfaceName` | `string`                     | —              | TypeScript interface name for code generation.     |
| `defaultValue`  | `string \| (data) => string` | —              | Default value (must match an option).              |
| `admin.layout`  | `"horizontal" \| "vertical"` | `"horizontal"` | Button layout direction.                           |

```typescript
radio({
  name: "size",
  options: [option("S"), option("M"), option("L"), option("XL")],
  admin: { layout: "horizontal" },
});
```

***

### chips

Free-form multi-value string field that stores an array of unique strings. Renders as interactive chips/tags. Duplicate entries are automatically prevented.

```typescript
chips({ name: "tags" })
```

| Option              | Type                             | Default                         | Description                                                            |
| ------------------- | -------------------------------- | ------------------------------- | ---------------------------------------------------------------------- |
| `defaultValue`      | `string[] \| (data) => string[]` | —                               | Initial chips.                                                         |
| `maxChips`          | `number`                         | —                               | Maximum number of chips allowed. The add input is hidden once reached. |
| `minChips`          | `number`                         | —                               | Minimum number of chips required (validation).                         |
| `admin.placeholder` | `string`                         | `"Type and press Enter to add"` | Placeholder text for the input.                                        |

```typescript
chips({
  name: "keywords",
  required: true,
  minChips: 1,
  maxChips: 10,
});
```

***

## Media

### upload

Reference files from upload-enabled collections. Works like `relationship` but with media-specific UI (thumbnails) and a richer `filterOptions` query schema.

```typescript
upload({ name: "featuredImage", relationTo: "media" })
```

| Option                 | Type                                                                        | Default                   | Description                                                                             |
| ---------------------- | --------------------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------- |
| `relationTo`           | `string \| string[]`                                                        | —                         | **Required.** Upload-collection slug or array of slugs (polymorphic).                   |
| `hasMany`              | `boolean`                                                                   | `false`                   | Allow multiple file selections.                                                         |
| `minRows`              | `number`                                                                    | —                         | Min uploads when `hasMany: true`.                                                       |
| `maxRows`              | `number`                                                                    | —                         | Max uploads when `hasMany: true`.                                                       |
| `maxDepth`             | `number`                                                                    | `1`                       | Population depth for the related document.                                              |
| `maxFileSize`          | `number`                                                                    | —                         | Max file size in bytes (rejected before upload).                                        |
| `mimeTypes`            | `string`                                                                    | —                         | Comma-separated allowed MIME types (e.g. `"image/*"` or `"image/png,application/pdf"`). |
| `filterOptions`        | `UploadFilterQuery \| (args) => boolean \| UploadFilterQuery \| Promise<…>` | —                         | Filter available uploads. Static query or dynamic function.                             |
| `defaultValue`         | See type union                                                              | —                         | Static default ID, array of IDs, polymorphic ref, or function.                          |
| `admin.allowCreate`    | `boolean`                                                                   | `true`                    | Allow uploading new files from the field.                                               |
| `admin.allowEdit`      | `boolean`                                                                   | `true`                    | Allow editing upload metadata.                                                          |
| `admin.isSortable`     | `boolean`                                                                   | `true`                    | Drag-and-drop reorder when `hasMany: true`.                                             |
| `admin.displayPreview` | `boolean`                                                                   | inherited from collection | Show thumbnail preview.                                                                 |

`filterOptions` static-query operators: `equals`, `not_equals`, `contains`, `in`, `not_in`, `exists` (strings); `equals`, `not_equals`, `greater_than`, `greater_than_equal`, `less_than`, `less_than_equal`, `exists` (numbers). Available filterable keys: `mimeType`, `filesize`, `width`, `height`, `filename`, `alt`, plus any custom field on the upload collection.

```typescript
// Gallery with constraints
upload({
  name: "gallery",
  relationTo: "media",
  hasMany: true,
  maxRows: 10,
  filterOptions: { mimeType: { contains: "image" } },
});

// Polymorphic uploads
upload({
  name: "attachment",
  relationTo: ["media", "documents"],
});

// Dynamic filter — require HD images for hero
upload({
  name: "heroImage",
  relationTo: "media",
  filterOptions: () => ({
    mimeType: { contains: "image" },
    width: { greater_than_equal: 1920 },
    height: { greater_than_equal: 1080 },
  }),
});
```

***

## Relationship

### relationship

Reference documents from other collections. Supports single, multi, and polymorphic relationships.

```typescript
relationship({ name: "author", relationTo: "users", required: true })
```

| Option              | Type                                                                                    | Default    | Description                                                                           |
| ------------------- | --------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------- |
| `relationTo`        | `string \| string[]`                                                                    | —          | **Required.** Target collection slug or array of slugs (polymorphic).                 |
| `hasMany`           | `boolean`                                                                               | `false`    | Allow multiple document references.                                                   |
| `minRows`           | `number`                                                                                | —          | Min selections when `hasMany: true`.                                                  |
| `maxRows`           | `number`                                                                                | —          | Max selections when `hasMany: true`.                                                  |
| `maxDepth`          | `number`                                                                                | `1`        | Population depth for nested relationships.                                            |
| `filterOptions`     | `RelationshipFilterQuery \| (args) => boolean \| RelationshipFilterQuery \| Promise<…>` | —          | Filter available documents. Static query or dynamic function.                         |
| `defaultValue`      | See type union                                                                          | —          | Static default ID, array of IDs, polymorphic ref, or function.                        |
| `admin.allowCreate` | `boolean`                                                                               | `true`     | Allow creating new documents from the field.                                          |
| `admin.allowEdit`   | `boolean`                                                                               | `true`     | Allow editing related documents.                                                      |
| `admin.isSortable`  | `boolean`                                                                               | `true`     | Enable drag-and-drop reorder (multi only).                                            |
| `admin.sortOptions` | `string \| Record<string, string>`                                                      | —          | Default sort. Prefix with `-` for descending. Per-collection mapping for polymorphic. |
| `admin.appearance`  | `"select" \| "drawer"`                                                                  | `"select"` | Picker UI style. Use `"drawer"` for large lists.                                      |

```typescript
// Has many
relationship({
  name: "categories",
  relationTo: "categories",
  hasMany: true,
  maxRows: 5,
});

// Polymorphic
relationship({
  name: "relatedContent",
  relationTo: ["posts", "pages", "products"],
  hasMany: true,
});

// Self-reference, exclude self
relationship({
  name: "parent",
  relationTo: "pages",
  filterOptions: ({ id }) =>
    id ? { id: { not_equals: id } } : true,
});

// Drawer picker for big lists
relationship({
  name: "featuredProducts",
  relationTo: "products",
  hasMany: true,
  admin: { appearance: "drawer", sortOptions: "-sales" },
});
```

**Gotchas:** when both `filterOptions` and `validate` are set, the filter is **not** automatically re-validated server-side — include the same constraint in `validate` if you need it enforced.

***

## Layout

### repeater

Repeatable sets of fields. Each row contains the same field structure. Rows can be added, removed, and reordered.

```typescript
repeater({
  name: "links",
  labels: { singular: "Link", plural: "Links" },
  fields: [
    text({ name: "label", required: true }),
    text({ name: "url", required: true }),
  ],
})
```

| Option                      | Type                                                 | Default | Description                                            |
| --------------------------- | ---------------------------------------------------- | ------- | ------------------------------------------------------ |
| `fields`                    | `FieldConfig[]`                                      | —       | **Required.** Field definitions for each row.          |
| `minRows`                   | `number`                                             | —       | Minimum rows.                                          |
| `maxRows`                   | `number`                                             | —       | Maximum rows. The Add button is disabled when reached. |
| `labels`                    | `{ singular?: string; plural?: string }`             | —       | Custom row labels (e.g. `"Link"` / `"Links"`).         |
| `interfaceName`             | `string`                                             | —       | TypeScript interface name for code generation.         |
| `dbName`                    | `string`                                             | Auto    | Custom database table name.                            |
| `virtual`                   | `boolean`                                            | `false` | Skip database storage; field exists only in API.       |
| `defaultValue`              | `RepeaterRowValue[] \| (data) => RepeaterRowValue[]` | —       | Static default rows.                                   |
| `admin.initCollapsed`       | `boolean`                                            | `false` | Render rows initially collapsed.                       |
| `admin.isSortable`          | `boolean`                                            | `true`  | Enable drag-and-drop reorder.                          |
| `admin.components.RowLabel` | `React.ComponentType<RepeaterRowLabelProps>`         | —       | Custom row-label component.                            |

```typescript
// FAQ with custom row labels
repeater({
  name: "faq",
  labels: { singular: "Question", plural: "Questions" },
  fields: [
    text({ name: "question", required: true }),
    richText({ name: "answer", required: true }),
  ],
  admin: {
    initCollapsed: true,
    // RowLabel component must be provided as a path string when using
    // a server-rendered config; React component in client contexts.
  },
});
```

> **Note:** Use `repeater()` to define this field. The internal `type` is `"repeater"`. (Some other CMSes call this concept "array".)

**Cross-links:** [Field Groups](https://nextlyhq.com/docs/configuration/field-groups.md#repeatable-single-field-group) covers the alternative repeatable field-group pattern when you want a typed schema instead of inline fields.

***

### group

Organize related fields together. Named groups create nested data; groups without a `name` are presentational only.

```typescript
group({
  name: "seo",
  fields: [
    text({ name: "metaTitle", maxLength: 60 }),
    textarea({ name: "metaDescription", maxLength: 160 }),
  ],
})
// Stored data: { seo: { metaTitle: "...", metaDescription: "..." } }
```

| Option             | Type                                     | Default | Description                                                                                 |
| ------------------ | ---------------------------------------- | ------- | ------------------------------------------------------------------------------------------- |
| `name`             | `string`                                 | —       | If provided, fields are nested under this property. If omitted, the group is purely visual. |
| `fields`           | `FieldConfig[]`                          | —       | **Required.** Nested field definitions.                                                     |
| `interfaceName`    | `string`                                 | —       | TypeScript interface name.                                                                  |
| `dbName`           | `string`                                 | Auto    | Custom database column/table name.                                                          |
| `virtual`          | `boolean`                                | `false` | Skip database storage.                                                                      |
| `defaultValue`     | `Record<string, unknown> \| (data) => …` | —       | Default values for nested fields (named groups only).                                       |
| `admin.hideGutter` | `boolean`                                | `false` | Remove the left-side visual gutter.                                                         |

```typescript
// Presentational group — no data nesting; fields stored at parent level
group({
  label: "Display Settings",
  fields: [
    checkbox({ name: "showTitle", defaultValue: true }),
    checkbox({ name: "showDate", defaultValue: true }),
  ],
  admin: { hideGutter: true },
});
```

**Gotchas:** named groups create real nested objects in your data — keep that in mind for queries and migrations. Presentational groups are pure UI; their fields share the parent's data namespace.

***

## Component

### component

Embed reusable [field groups](https://nextlyhq.com/docs/configuration/field-groups.md) inside collections, singles, or other field groups. Three usage modes are supported.

```typescript
// Single fixed type
fieldGroup({ name: "seo", component: "seo" })

// Dynamic zone
fieldGroup({
  name: "layout",
  components: ["hero", "cta", "content-block"],
  repeatable: true,
})

// Repeatable single type
fieldGroup({
  name: "features",
  component: "feature-card",
  repeatable: true,
  minRows: 1,
  maxRows: 12,
})
```

| Option                | Type       | Default | Description                                                                               |
| --------------------- | ---------- | ------- | ----------------------------------------------------------------------------------------- |
| `component`           | `string`   | —       | Single mode: one specific component slug. Mutually exclusive with `components`.           |
| `components`          | `string[]` | —       | Dynamic-zone mode: array of allowed component slugs. Mutually exclusive with `component`. |
| `repeatable`          | `boolean`  | `false` | Allow multiple instances (array mode).                                                    |
| `minRows`             | `number`   | —       | Min instances when `repeatable: true`.                                                    |
| `maxRows`             | `number`   | —       | Max instances when `repeatable: true`.                                                    |
| `admin.initCollapsed` | `boolean`  | `false` | Start instances collapsed.                                                                |
| `admin.isSortable`    | `boolean`  | `true`  | Drag-and-drop reorder when `repeatable: true`.                                            |

**Gotchas:** `component` and `components` are mutually exclusive; you'll get a startup error if both are set. Component nesting is capped at depth 3.

**Cross-links:** see [Field Groups](https://nextlyhq.com/docs/configuration/field-groups.md) for the `defineFieldGroup()` reference and [the three modes](https://nextlyhq.com/docs/configuration/field-groups.md#using-field-groups-in-collections-and-singles) explained side-by-side.

***

## Advanced

### json

Store arbitrary JSON data with an in-browser code editor. Supports JSON Schema validation.

```typescript
json({ name: "metadata" })
```

| Option                                 | Type                             | Default | Description                                         |
| -------------------------------------- | -------------------------------- | ------- | --------------------------------------------------- |
| `jsonSchema`                           | `JSONSchemaDefinition`           | —       | Inline JSON Schema for validation and editor hints. |
| `defaultValue`                         | Any valid JSON value or function | —       | Default value.                                      |
| `admin.editorOptions.height`           | `number \| string`               | `300`   | Editor height.                                      |
| `admin.editorOptions.minHeight`        | `number`                         | `100`   | Minimum height.                                     |
| `admin.editorOptions.maxHeight`        | `number`                         | —       | Maximum height.                                     |
| `admin.editorOptions.lineNumbers`      | `boolean`                        | `true`  | Show line numbers.                                  |
| `admin.editorOptions.folding`          | `boolean`                        | `true`  | Enable code folding.                                |
| `admin.editorOptions.wordWrap`         | `boolean`                        | `false` | Enable word wrapping.                               |
| `admin.editorOptions.minimap`          | `boolean`                        | `false` | Show code minimap.                                  |
| `admin.editorOptions.tabSize`          | `number`                         | `2`     | Tab size in spaces.                                 |
| `admin.editorOptions.formatOnBlur`     | `boolean`                        | `true`  | Auto-prettify on blur.                              |
| `admin.editorOptions.validateOnChange` | `boolean`                        | `true`  | Real-time syntax validation.                        |

**Database storage:** PostgreSQL uses `JSONB`, MySQL uses `JSON`, SQLite uses `TEXT`. Schema validation happens at the application level so behavior is consistent across adapters.

```typescript
json({
  name: "settings",
  jsonSchema: {
    type: "object",
    properties: {
      theme: { type: "string", enum: ["light", "dark", "system"] },
      maxItems: { type: "number", minimum: 1 },
    },
    required: ["theme"],
  },
});
```

***

## Common options across fields

Every field type inherits the following from `BaseFieldConfig`. Where a specific field's reference table above doesn't repeat them, they still apply.

### Identity

| Option      | Type                      | Default          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ----------- | ------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`      | `string`                  | —                | **Required.** Unique identifier. Lowercase, underscores or numbers — no hyphens, no spaces, no SQL reserved words.                                                                                                                                                                                                                                                                                                                                                                     |
| `type`      | `FieldType`               | Set by helper    | Field type literal. The helper functions (`text`, `number`, etc.) set this for you.                                                                                                                                                                                                                                                                                                                                                                                                    |
| `label`     | `string`                  | Auto from `name` | Display label in the admin UI (e.g. `user_name` → `User Name`).                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `required`  | `boolean`                 | `false`          | Whether the field must have a non-null, non-empty value.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `unique`    | `boolean`                 | `false`          | Enforce database-level uniqueness.                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `index`     | `boolean`                 | `false`          | Create a single-field database index.                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `localized` | `boolean`                 | `false`          | **Reserved for a future release.** When implemented, will store separate values per locale.                                                                                                                                                                                                                                                                                                                                                                                            |
| `custom`    | `Record<string, unknown>` | —                | Arbitrary metadata for plugins or hooks. Not persisted.                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `virtual`   | `boolean`                 | `false`          | On a collection, Single or component: store nothing. The field gets no column, a value sent for it on create or update is dropped before the write, write validation skips it (so `required` and `validate` do not apply), and it is never localized. Compute its value in an `afterRead` hook. Group and repeater also accept `options.virtual`. Custom user fields are always stored, so user config validation refuses `virtual` on them. *Needs the release after 0.0.2-alpha.66.* |

### `admin`

Shared UI options:

| Option              | Type                                                  | Default  | Description                                                      |
| ------------------- | ----------------------------------------------------- | -------- | ---------------------------------------------------------------- |
| `position`          | `"sidebar"`                                           | —        | Place the field in the sidebar instead of the main content area. |
| `width`             | `"25%" \| "33%" \| "50%" \| "66%" \| "75%" \| "100%"` | `"100%"` | Field width in the form layout grid.                             |
| `description`       | `string`                                              | —        | Help text below the field label.                                 |
| `placeholder`       | `string`                                              | —        | Placeholder text when empty.                                     |
| `readOnly`          | `boolean`                                             | `false`  | Make the field read-only in the UI.                              |
| `disabled`          | `boolean`                                             | `false`  | Disable the input.                                               |
| `hidden`            | `boolean`                                             | `false`  | Hide the field from the UI entirely (still settable via API).    |
| `condition`         | `FieldCondition`                                      | —        | Conditionally show/hide based on another field's value.          |
| `className`         | `string`                                              | —        | Custom CSS class on the wrapper.                                 |
| `style`             | `Record<string, string>`                              | —        | Inline styles on the wrapper.                                    |
| `components.Field`  | `React.ComponentType<FieldComponentProps>`            | —        | Custom form-field renderer.                                      |
| `components.Cell`   | `React.ComponentType<CellComponentProps>`             | —        | Custom list/table cell renderer.                                 |
| `components.Filter` | `React.ComponentType<FilterComponentProps>`           | —        | Custom list-view filter UI.                                      |

#### Conditional logic

```typescript
text({
  name: "externalUrl",
  admin: {
    condition: {
      field: "linkType",
      equals: "external",
    },
  },
});
```

Available `FieldCondition` operators: `equals`, `notEquals`, `contains`, `exists`.

### `access`

Field-level access control — granular permissions per CRUD operation.

```typescript
text({
  name: "internalNotes",
  access: {
    create: ({ permissions }) => permissions.includes("users:update"),
    read: ({ permissions }) => permissions.includes("users:update"),
    update: ({ permissions }) => permissions.includes("users:update"),
  },
});
```

Each function receives `{ req, id?, data?, permissions, roles, principal }` and returns `boolean | Promise<boolean>`. `permissions` is the caller's, as `resource:action`; decide by it rather than by a role's name. Field-level access is checked **in addition to** collection/single access.

### `hooks`

Field-level lifecycle hooks. Each hook is an array of handlers run in order.
On a collection write and on a direct read, each phase runs **after** the
collection-level hook of the same name; see the
[execution order](https://nextlyhq.com/docs/configuration/collections.md#execution-order) for how the
two levels interleave. Two exceptions: a Single's field-level `afterChange`
runs **before** its entity-level `afterUpdate`, so the entity hook sees what the
field hooks produced and not the other way round; and a row reached through
relationship expansion runs only the target's field-level `afterRead`, with no
collection-level `afterRead` before it.

| Hook             | When it runs                                                                         |
| ---------------- | ------------------------------------------------------------------------------------ |
| `beforeValidate` | Before validation, after the collection's `beforeValidate`. Can transform the value. |
| `beforeChange`   | After the collection's `beforeChange`. The last hook before the database write.      |
| `afterChange`    | After the database write.                                                            |
| `afterRead`      | After reading from the database. Can transform the returned value.                   |

Every handler receives its own `value` and a `data` object at the field's own
nesting level: for a field inside a group or a repeater, the containing row
rather than the top-level document.

**Before the write** (`beforeValidate`, `beforeChange`), `data` is the record
as transformed so far, not a copy of what the caller sent. On an ordinary
create that means the request with field defaults applied before any hook
runs, constant and function alike (see [`defaultValue`](#defaultvalue)), so a
defaulted field's hook runs on a value nobody submitted, and a
`slug` and title generated after the collection-level `beforeValidate` and
before the first field-level hook, so a field hook can read them and a
collection hook in that phase cannot. Field-level `access.create` and
`access.update` rules are applied just before the field hooks, and a denied
field is removed from the record, so none of this reaches a field the caller
may not write: not its default, and not a generated `slug` or title. A bulk create (`createMany`) and a create
inside a transaction start from the submitted body: no defaults are applied, an
omitted defaulted field stays absent, and its hooks do not run. On an update it
is the patch, not the stored row, so an unchanged sibling reads as `undefined`.
Do not read a key's presence as proof the caller supplied it. A `beforeChange` handler can read and change
siblings at that level. No collection-level phase runs after it, but a
sibling's own hook registered later in the same phase still can. Return the
value you want stored; touch siblings only when that is the intent. These
hooks run only when their field is present in the data: an update that omits
it, or an earlier hook that deletes the key, skips them.

**After the write** (`afterChange`) and **on a read** (`afterRead`), `data` is
the row, persisted or loaded, and a hook runs for every field present on that
row, whether or not the field changed. The rule that decides the rest: a field
whose key is absent from the row when the hooks run has no hook run, and these
are the things that remove a key first.

* **Password fields** are stripped from every read before `afterRead`, on
  collections and Singles alike, so a password field's `afterRead` never runs.
* **Denied fields.** On a read, collection or Single, field-level
  `access.read` rules are applied before the hooks, so a field the caller may
  not read is gone by then, and again after them, so a value a hook put back
  is removed.
* **Localized fields** live in the companion table and are merged into the
  `afterChange` row only when the write carried them, so a localized field
  omitted from a create, or unchanged on an update, has no `afterChange` run.
* **Field groups** are stored in their own table and removed from the row before
  the main-table write, and `afterChange` runs on the returned row without them,
  on collections and Singles and inside a caller's transaction. Their
  `beforeValidate` and `beforeChange` do run. The one write where a field
  group's `afterChange` runs is a working-draft update through the ordinary
  update path: on a collection with `versions: { drafts: true }`, an update to
  a published entry that names no `status` is held as a draft and `afterChange`
  runs over that assembled draft. A collection is not eligible to hold drafts
  when it or one of its field groups has a password field, or when a field
  group's schema cannot be resolved; a status-less update there writes the live
  row.

A value returned from `afterChange` reshapes the response and does not change
what was stored.

Core still normalises the record between the last pre-write hook and the
write: passwords are hashed and relationship and upload values reduced to ids
on collections and Singles alike, and a collection's `slug` is re-sanitised,
which a Single's is not. See the
[execution order](https://nextlyhq.com/docs/configuration/collections.md#execution-order) for what
runs where.

```typescript
text({
  name: "slug",
  hooks: {
    beforeValidate: [
      async ({ value, data }) => {
        if (!value && data?.title) {
          return data.title.toLowerCase().replace(/\s+/g, "-");
        }
        return value;
      },
    ],
  },
});
```

### `validate`

Custom validation function. Receives the typed field value plus `{ data, req }` and returns either `true` (valid) or an error-message string. Runs **after** the field's built-in validation.

```typescript
text({
  name: "username",
  validate: (value) => {
    if (value && !/^[a-z0-9_]+$/.test(String(value))) {
      return "Lowercase letters, numbers, and underscores only";
    }
    return true;
  },
});
```

### `defaultValue`

Static value or `(data) => value` function. The function form receives the document data being created so you can compute defaults from sibling fields. Each field type's signature accepts the value type appropriate for that field.

Both forms are applied on the server, so a default holds for every write
path that applies defaults, not only for the admin form. On an ordinary
collection create through the REST or Direct API, an omitted field takes its
default before any hook runs, at every depth: a group's children and each
supplied repeater row's children. A new field-group instance also fills its own
children's defaults, but LATER: a field group's rows are written in their own
pass after the entry's hooks have run, so the parent's hooks see such a child as
absent and cannot read or derive from the value that will be stored. A function
default is read from the live config, since a
collection's stored definition cannot hold a function, and it receives the
data built so far at its own level: the document for a top-level field, the
group or the row for a nested one. Rows are never invented; `minRows` is a
validation rule, not an instruction to create rows.

A field-group child takes its default the same way, constant or function. Its
fields are read from a stored definition, which cannot carry a function, so the
function form is read from the live config exactly as a collection's is. It
resolves against the instance being built, so a child may compute from a sibling
that was defaulted before it.

A Single's first read creates its row with the same defaults, top-level and
nested, with two exceptions: a default on a `fieldGroup` field itself is
evaluated and then dropped because that field has no column on the row, and a
password field's default, at any depth, is refused because that path never
hashes.

Bulk creates (`createMany`) and creates inside a transaction apply no defaults
to the entry's own fields; a new field-group instance on those writes still
takes its own defaults.

***

## Helper utilities

### `option(label, value?)`

Creates a `{ label, value }` option for `select` and `radio` fields. Auto-generates the value from the label by lowercasing and replacing spaces with underscores.

```typescript
import { option } from "nextly";

option("Draft");          // { label: "Draft", value: "draft" }
option("In Progress");    // { label: "In Progress", value: "in_progress" }
option("Active", "active");
```

## Next steps

* [Collections](https://nextlyhq.com/docs/configuration/collections.md) — define content types using fields
* [Singles](https://nextlyhq.com/docs/configuration/singles.md) — define single-document content using fields
* [Field Groups](https://nextlyhq.com/docs/configuration/field-groups.md) — create reusable field groups
* [Visual Schema Builder](https://nextlyhq.com/docs/schema-builder.md) — build collections, singles, and field groups visually
