Fields

Fields Overview

Vextro uses a type-safe field builder system to define both the Convex schema validator and the admin UI configuration from a single declaration. Every field is created through the f namespace, which provides builder functions for each supported field type.

The field builder syntax

Import f from vextro and use it inside your collection or block definition:

import { f, defineVextroCollection } from "vextro";

export const posts = defineVextroCollection({
  slug: "posts",
  label: "Posts",
  collectionType: "content",
  tableName: "posts",
  fields: {
    title: f.text({ required: true }),
    body: f.richText(),
    status: f.select({ options: ["draft", "published"] }),
    author: f.id("users", { required: true }),
  },
});

Each f.<type>() call returns a VextroFieldDefinition that contains a Convex validator and an admin UI config object. When required is false (the default), the validator is wrapped in v.optional() automatically.

Common options

Every field builder accepts these shared options:

OptionTypeDefaultDescription
requiredbooleanfalseWhen true, the Convex validator is required and the admin UI enforces a value.
labelstring | (data) => stringAuto-generatedCustom label for the admin UI. Accepts a static string or a function that receives the document data.
descriptionstring—Help text displayed below the field label.
readOnlybooleanfalseRenders the field as disabled in the admin UI.
defaultValueunknown—Default value when creating new documents. The value should match the field's type.
placeholderstring—Placeholder text for text-based inputs.
conditionFieldCondition—Show or hide the field based on sibling field values. Conditional fields are always optional in the schema.
copyablebooleanfalseShow a copy-to-clipboard button next to the field value. Useful for IDs, slugs, URLs, and API keys.
tooltipstring | { icon?: "info" | "warning" | "danger", text: string }—Tooltip icon next to label. String shorthand uses info icon.
sidebarbooleanfalsePlace the field in the document sidebar instead of the main content area.
sidebarSectionstring—Group the field under a named sidebar section. Built-in sections: "document", "actions". Fields without a sidebarSection go to "_default" and render without a collapsible wrapper.
sidebarCollapsedbooleanfalseStart the sidebar section collapsed for this field.
listColumnbooleanfalseInclude the field as a default column in collection list views.
listColumnWidth"auto" | "small" | "medium" | "large""auto"Column width preset for list views.
searchablebooleanfalseIndex the field value for full-text search.
viewTransitionbooleanfalseAnimate this field with CSS View Transitions when navigating between list and edit views.
readRolesstring[]—Roles that can see this field. When unset, all roles can read.
writeRolesstring[]—Roles that can edit this field. When unset, all roles can write. Users without write access see the field as read-only.
hiddenbooleanfalseCompletely hide the field from users who lack read access. When false, restricted fields are visible but values are omitted.
uniquebooleanfalseEnforce uniqueness for this field. Adds a by_fieldName Convex index and rejects create/update when another document has the same value. f.slug() defaults to true.
indexedbooleanfalseIdentical to unique. Adds a Convex index and enforces uniqueness.
widthnumber—Width as a percentage (1–100) when this field is inside an f.row(). Fields without an explicit width share the remaining space equally. See Row layout.
hooksFieldHooks—Field-level hooks for value transformation. Supports beforeChange (transform before write), afterRead (transform after read), afterChange (side effects after write), and beforeDuplicate (transform copied values during duplication). See Hooks.

How fields map to Convex validators

Each builder produces a specific Convex validator. When required is false, the validator is wrapped in v.optional().

BuilderConvex Validator
f.text()v.string()
f.textarea()v.string()
f.number()v.number()
f.email()v.string()
f.url()v.string()
f.slug()v.string()
f.select()v.union(v.literal(...)) per option
f.radio()v.union(v.literal(...)) per option
f.checkbox()v.boolean()
f.date()v.string() (ISO date)
f.datetime()v.number() (Unix ms)
f.id("table")v.id("table")
f.json()v.any()
f.group()v.object({...}) from nested fields
f.array()v.array(v.object({...})) from nested fields

Conditional display

All fields accept a condition option that controls visibility in the admin UI. Conditional fields are always stored as optional in the Convex schema, even when required: true is set. The required validation only applies when the field is visible.

linkType: f.select({
  options: ["internal", "external"],
  required: true,
}),
externalUrl: f.url({
  required: true,
  condition: { field: "linkType", equals: "external" },
}),

Conditions support equals, notEquals, in, notIn, exists, comparison operators, and logical combinators (and, or, not).

Previous
Routing
Next
Text