Fields
Complete reference for every field type, their options, and usage patterns.
Fields define the shape of your content. Every collection, single, and component 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.
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
selectfield. If you want a Draft / Published lifecycle on a collection, use the built-instatus: trueflag ondefineCollection(or the Advanced-tab toggle in the Schema Builder) -- not a hand-rolledselectfield. See Draft / Published status.
Source of truth:
packages/nextly/src/collections/fields/types/(one file per field type) andpackages/nextly/src/collections/fields/helpers.ts(the helper exports).
Field categories
Eight categories partition the field inventory.
| Category | Fields |
|---|---|
| Basic | text, textarea, richText, email, password, code, number, checkbox, date |
| Selection | select, radio, chips |
| Media | upload |
| Relationship | relationship |
| Layout | repeater, group |
| Component | component |
| Advanced | json |
| Virtual | join |
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).
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"). |
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.
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. |
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.
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 |
// 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.
Specialised text input with built-in email-format validation.
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.
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.
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:
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.
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.
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.
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. |
number({ name: "rating", min: 1, max: 5, admin: { step: 1 } });checkbox
Boolean true/false toggle.
checkbox({ name: "published", defaultValue: false })| Option | Type | Default | Description |
|---|---|---|---|
defaultValue | boolean | (data) => boolean | — | Initial value. If required: true and unset, defaults to false. |
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.
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. |
// 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.
import { select, option } from "nextly";
select({
name: "priority",
options: [option("Low"), option("Medium"), option("High")],
})Use
selectfor arbitrary categorical options (priority, region, theme, etc.). For a collection's Draft / Published lifecycle, prefer the built-instatus: trueflag ondefineCollection-- see Draft / Published status.
| 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.
option("Draft"); // { label: "Draft", value: "draft" }
option("In Review"); // { label: "In Review", value: "in_review" }
option("Active", "active");// 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.
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. |
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.
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. |
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.
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.
// 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.
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. |
// 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.
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. |
// 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 internaltypeis"repeater". (Some other CMSes call this concept "array".)
Cross-links: Components covers the alternative repeatable-component 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.
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. |
// 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 components inside collections, singles, or other components. Three usage modes are supported.
// Single fixed type
component({ name: "seo", component: "seo" })
// Dynamic zone
component({
name: "layout",
components: ["hero", "cta", "content-block"],
repeatable: true,
})
// Repeatable single type
component({
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 Components for the defineComponent() reference and the three modes explained side-by-side.
Advanced
json
Store arbitrary JSON data with an in-browser code editor. Supports JSON Schema validation.
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.
json({
name: "settings",
jsonSchema: {
type: "object",
properties: {
theme: { type: "string", enum: ["light", "dark", "system"] },
maxItems: { type: "number", minimum: 1 },
},
required: ["theme"],
},
});Virtual
join
Display reverse relationships — entries from another collection that reference the current document. Join fields are read-only and do not store data; they query at read time.
{
name: "posts",
type: "join",
collection: "posts",
on: "category",
}There is no helper function for join — declare it as a plain object with type: "join".
| Option | Type | Default | Description |
|---|---|---|---|
type | "join" | — | Required. Field type literal. |
name | string | — | Required. Field identifier (no DB column is created). |
collection | string | — | Required. Collection containing the referencing field. |
on | string | — | Required. Field name in collection that references this document. Supports dot notation (e.g. "metadata.author"). |
where | Record<string, unknown> | — | Additional filter for joined entries (Nextly Where syntax). |
defaultLimit | number | 10 | Max entries to display. Set 0 to display all. |
defaultSort | string | — | Sort field. Prefix with - for descending. |
maxDepth | number | 1 | Population depth for relationships in joined entries. |
label | string | Auto | Display label. |
admin.allowNavigation | boolean | true | Make entries clickable links. |
admin.allowCreate | boolean | false | Show a "Create New" button (pre-fills the relationship). |
admin.defaultColumns | string[] | — | Columns to display in the list. |
// On a Categories collection — show all posts in this category
{
name: "posts",
type: "join",
label: "Posts in this Category",
collection: "posts",
on: "category",
defaultSort: "-createdAt",
admin: {
defaultColumns: ["title", "status", "createdAt"],
},
}Gotchas: join fields are read-only; the related entries are still edited from their own collection's edit page. Only name, label, and admin are common with other field types — required, unique, defaultValue, validate, access, and hooks do not apply.
Common options across fields
Every field type (except join, which is virtual) 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. |
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
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.
text({
name: "internalNotes",
access: {
create: ({ req }) => req.user?.role === "admin",
read: ({ req }) => req.user?.role === "admin",
update: ({ req }) => req.user?.role === "admin",
},
});Each function receives { req, id?, data? } and returns boolean | Promise<boolean>. 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.
| Hook | When it runs |
|---|---|
beforeValidate | Before validation. Can transform the value. |
beforeChange | Before the database write. |
afterChange | After the database write. |
afterRead | After reading from the database. Can transform the returned value. |
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.
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.
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.
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 — define content types using fields
- Singles — define single-document content using fields
- Components — create reusable field groups
- Visual Schema Builder — build collections, singles, and components visually