You're reading docs for Nextly Alpha. APIs may change between releases.

Configuration

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 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.

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

Eight categories partition the field inventory.

CategoryFields
Basictext, textarea, richText, email, password, code, number, checkbox, date
Selectionselect, radio, chips
Mediaupload
Relationshiprelationship
Layoutrepeater, group
Componentcomponent
Advancedjson
Virtualjoin

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 })
OptionTypeDefaultDescription
namestringRequired. Unique field identifier. Lowercase, no spaces.
minLengthnumberMinimum string length.
maxLengthnumberMaximum string length. Also sets the DB column size.
hasManybooleanfalseAccept an array of strings.
minRowsnumberMin items when hasMany: true.
maxRowsnumberMax items when hasMany: true.
defaultValuestring | string[] | (data) => …Static default or function.
admin.autoCompletestringHTML 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 })
OptionTypeDefaultDescription
minLengthnumberMinimum string length.
maxLengthnumberMaximum string length.
defaultValuestring | (data) => stringStatic default or function.
admin.rowsnumber3Number 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" })
OptionTypeDefaultDescription
featuresRichTextFeature[]See "Default features" belowEnabled editor features. Pass [] for plain text.
defaultValueRichTextValue | (data) => RichTextValueStatic default or function.
admin.hideToolbarbooleanfalseHide the editor toolbar (keyboard shortcuts still work).

Default features when features is omitted: bold, italic, underline, strikethrough, code, h1h4, orderedList, unorderedList, indent, blockquote, link.

All available features (RichTextFeature union):

CategoryFeatures
Formattingbold, italic, underline, strikethrough, code, subscript, superscript
Text stylingfontFamily, fontSize, fontColor, bgColor
Headingsh1, h2, h3, h4, h5, h6
ListsorderedList, unorderedList, checkList, indent
Links and medialink, upload, relationship
Advancedtable, horizontalRule, codeBlock, align
Rich mediavideo, 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.


email

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

email({ name: "email", required: true, unique: true })
OptionTypeDefaultDescription
defaultValuestring | (data) => stringStatic default or function.
admin.autoCompletestring"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,
})
OptionTypeDefaultDescription
minLengthnumberMinimum password length. (Helper-level default is none; documented recommended minimum is 8.)
maxLengthnumberMaximum password length. Most hashing algorithms cap around 72–128 bytes.
defaultValuestring | (data) => stringStatic default. Setting one is rarely a good idea security-wise.
admin.autoComplete"new-password" | "current-password" | "off""new-password"HTML autocomplete attribute.
admin.showStrengthIndicatorbooleanfalseShow 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" },
})
OptionTypeDefaultDescription
defaultValuestring | (data) => stringStatic default or function.
admin.languageCodeLanguage"plaintext"Syntax-highlighting language.
admin.editorOptions.lineNumbersbooleantrueShow line numbers.
admin.editorOptions.wordWrapbooleanfalseEnable word wrapping.
admin.editorOptions.tabSizenumber2Tab width in spaces.
admin.editorOptions.useTabsbooleanfalseUse tabs instead of spaces.
admin.editorOptions.minHeightnumber200Minimum editor height in px.
admin.editorOptions.maxHeightnumberMaximum editor height in px.
admin.editorOptions.fontSizenumber14Font size in px.
admin.editorOptions.fontFamilystring"monospace"Editor font family.
admin.editorOptions.foldingbooleantrueEnable code folding.
admin.editorOptions.matchBracketsbooleantrueHighlight matching brackets.
admin.editorOptions.autoCloseBracketsbooleantrueAuto-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 } })
OptionTypeDefaultDescription
minnumberMinimum allowed value.
maxnumberMaximum allowed value.
hasManybooleanfalseAccept an array of numbers.
minRowsnumberMin items when hasMany: true.
maxRowsnumberMax items when hasMany: true.
defaultValuenumber | number[] | (data) => …Static default or function.
admin.stepnumber1Increment step for spinner buttons.
admin.placeholderstringPlaceholder text.
number({ name: "rating", min: 1, max: 5, admin: { step: 1 } });

checkbox

Boolean true/false toggle.

checkbox({ name: "published", defaultValue: false })
OptionTypeDefaultDescription
defaultValueboolean | (data) => booleanInitial 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" })
OptionTypeDefaultDescription
defaultValuestring | Date | (data) => string | DateStatic default or function.
admin.date.pickerAppearance"dayOnly" | "dayAndTime" | "timeOnly" | "monthOnly""dayOnly"Picker mode.
admin.date.displayFormatstringDisplay format string (date-fns format).
admin.date.monthsToShow1 | 21Months visible in the picker (max 2).
admin.date.minDateDate | stringEarliest selectable date.
admin.date.maxDateDate | stringLatest selectable date.
admin.date.minTimeDate | stringEarliest selectable time (when picker includes time).
admin.date.maxTimeDate | stringLatest selectable time.
admin.date.timeIntervalsnumber30Time step in minutes.
admin.date.timeFormatstring"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 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.

OptionTypeDefaultDescription
optionsSelectOption[]Required. Array of { label, value } objects.
hasManybooleanfalseAllow multiple selections.
enumNamestringAutoCustom SQL enum name.
interfaceNamestringTypeScript interface name for code generation.
filterOptions(args) => SelectOption[]Dynamically filter options based on data, sibling data, and user.
defaultValuestring | string[] | (data) => …Default value(s).
admin.isClearablebooleanfalseShow a clear button.
admin.isSortablebooleanfalseEnable 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")],
})
OptionTypeDefaultDescription
optionsSelectOption[]Required. Array of { label, value } objects.
enumNamestringAutoCustom SQL enum name.
interfaceNamestringTypeScript interface name for code generation.
defaultValuestring | (data) => stringDefault 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" })
OptionTypeDefaultDescription
defaultValuestring[] | (data) => string[]Initial chips.
maxChipsnumberMaximum number of chips allowed. The add input is hidden once reached.
minChipsnumberMinimum number of chips required (validation).
admin.placeholderstring"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" })
OptionTypeDefaultDescription
relationTostring | string[]Required. Upload-collection slug or array of slugs (polymorphic).
hasManybooleanfalseAllow multiple file selections.
minRowsnumberMin uploads when hasMany: true.
maxRowsnumberMax uploads when hasMany: true.
maxDepthnumber1Population depth for the related document.
maxFileSizenumberMax file size in bytes (rejected before upload).
mimeTypesstringComma-separated allowed MIME types (e.g. "image/*" or "image/png,application/pdf").
filterOptionsUploadFilterQuery | (args) => boolean | UploadFilterQuery | Promise<…>Filter available uploads. Static query or dynamic function.
defaultValueSee type unionStatic default ID, array of IDs, polymorphic ref, or function.
admin.allowCreatebooleantrueAllow uploading new files from the field.
admin.allowEditbooleantrueAllow editing upload metadata.
admin.isSortablebooleantrueDrag-and-drop reorder when hasMany: true.
admin.displayPreviewbooleaninherited from collectionShow 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 })
OptionTypeDefaultDescription
relationTostring | string[]Required. Target collection slug or array of slugs (polymorphic).
hasManybooleanfalseAllow multiple document references.
minRowsnumberMin selections when hasMany: true.
maxRowsnumberMax selections when hasMany: true.
maxDepthnumber1Population depth for nested relationships.
filterOptionsRelationshipFilterQuery | (args) => boolean | RelationshipFilterQuery | Promise<…>Filter available documents. Static query or dynamic function.
defaultValueSee type unionStatic default ID, array of IDs, polymorphic ref, or function.
admin.allowCreatebooleantrueAllow creating new documents from the field.
admin.allowEditbooleantrueAllow editing related documents.
admin.isSortablebooleantrueEnable drag-and-drop reorder (multi only).
admin.sortOptionsstring | 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 }),
  ],
})
OptionTypeDefaultDescription
fieldsFieldConfig[]Required. Field definitions for each row.
minRowsnumberMinimum rows.
maxRowsnumberMaximum rows. The Add button is disabled when reached.
labels{ singular?: string; plural?: string }Custom row labels (e.g. "Link" / "Links").
interfaceNamestringTypeScript interface name for code generation.
dbNamestringAutoCustom database table name.
virtualbooleanfalseSkip database storage; field exists only in API.
defaultValueRepeaterRowValue[] | (data) => RepeaterRowValue[]Static default rows.
admin.initCollapsedbooleanfalseRender rows initially collapsed.
admin.isSortablebooleantrueEnable drag-and-drop reorder.
admin.components.RowLabelReact.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 internal type is "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: "..." } }
OptionTypeDefaultDescription
namestringIf provided, fields are nested under this property. If omitted, the group is purely visual.
fieldsFieldConfig[]Required. Nested field definitions.
interfaceNamestringTypeScript interface name.
dbNamestringAutoCustom database column/table name.
virtualbooleanfalseSkip database storage.
defaultValueRecord<string, unknown> | (data) => …Default values for nested fields (named groups only).
admin.hideGutterbooleanfalseRemove 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,
})
OptionTypeDefaultDescription
componentstringSingle mode: one specific component slug. Mutually exclusive with components.
componentsstring[]Dynamic-zone mode: array of allowed component slugs. Mutually exclusive with component.
repeatablebooleanfalseAllow multiple instances (array mode).
minRowsnumberMin instances when repeatable: true.
maxRowsnumberMax instances when repeatable: true.
admin.initCollapsedbooleanfalseStart instances collapsed.
admin.isSortablebooleantrueDrag-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" })
OptionTypeDefaultDescription
jsonSchemaJSONSchemaDefinitionInline JSON Schema for validation and editor hints.
defaultValueAny valid JSON value or functionDefault value.
admin.editorOptions.heightnumber | string300Editor height.
admin.editorOptions.minHeightnumber100Minimum height.
admin.editorOptions.maxHeightnumberMaximum height.
admin.editorOptions.lineNumbersbooleantrueShow line numbers.
admin.editorOptions.foldingbooleantrueEnable code folding.
admin.editorOptions.wordWrapbooleanfalseEnable word wrapping.
admin.editorOptions.minimapbooleanfalseShow code minimap.
admin.editorOptions.tabSizenumber2Tab size in spaces.
admin.editorOptions.formatOnBlurbooleantrueAuto-prettify on blur.
admin.editorOptions.validateOnChangebooleantrueReal-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".

OptionTypeDefaultDescription
type"join"Required. Field type literal.
namestringRequired. Field identifier (no DB column is created).
collectionstringRequired. Collection containing the referencing field.
onstringRequired. Field name in collection that references this document. Supports dot notation (e.g. "metadata.author").
whereRecord<string, unknown>Additional filter for joined entries (Nextly Where syntax).
defaultLimitnumber10Max entries to display. Set 0 to display all.
defaultSortstringSort field. Prefix with - for descending.
maxDepthnumber1Population depth for relationships in joined entries.
labelstringAutoDisplay label.
admin.allowNavigationbooleantrueMake entries clickable links.
admin.allowCreatebooleanfalseShow a "Create New" button (pre-fills the relationship).
admin.defaultColumnsstring[]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

OptionTypeDefaultDescription
namestringRequired. Unique identifier. Lowercase, underscores or numbers — no hyphens, no spaces, no SQL reserved words.
typeFieldTypeSet by helperField type literal. The helper functions (text, number, etc.) set this for you.
labelstringAuto from nameDisplay label in the admin UI (e.g. user_nameUser Name).
requiredbooleanfalseWhether the field must have a non-null, non-empty value.
uniquebooleanfalseEnforce database-level uniqueness.
indexbooleanfalseCreate a single-field database index.
localizedbooleanfalseReserved for a future release. When implemented, will store separate values per locale.
customRecord<string, unknown>Arbitrary metadata for plugins or hooks. Not persisted.

admin

Shared UI options:

OptionTypeDefaultDescription
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.
descriptionstringHelp text below the field label.
placeholderstringPlaceholder text when empty.
readOnlybooleanfalseMake the field read-only in the UI.
disabledbooleanfalseDisable the input.
hiddenbooleanfalseHide the field from the UI entirely (still settable via API).
conditionFieldConditionConditionally show/hide based on another field's value.
classNamestringCustom CSS class on the wrapper.
styleRecord<string, string>Inline styles on the wrapper.
components.FieldReact.ComponentType<FieldComponentProps>Custom form-field renderer.
components.CellReact.ComponentType<CellComponentProps>Custom list/table cell renderer.
components.FilterReact.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.

HookWhen it runs
beforeValidateBefore validation. Can transform the value.
beforeChangeBefore the database write.
afterChangeAfter the database write.
afterReadAfter 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