relationship field
A relationship() field stores one or more Convex Id references pointing to
documents in another registered collection. The generated Convex schema always
emits v.array(v.id("slug")) — hasMany is a UI-only hint that controls
whether the admin picker allows a single selection or multiple. VexCMS
auto-generates a .index("by_<fieldKey>", ["<fieldKey>"]) for every relationship
field, and adds a .searchIndex("search_<useAsTitle>", …) on the target
collection whenever one of its fields is referenced — so the picker’s search
works out of the box with no extra config.
After running vex generate, passing an unregistered collection slug is a
compile-time error.
Config options
Section titled “Config options”| Option | Type | Default | Description |
|--------|------|---------|-------------|
| collection.slug | CollectionSlug | (required) | Slug of the target collection. Must be a registered collection in defineConfig. |
| hasMany | boolean | false | UI hint — false shows a single-selection picker, true shows a multi-selection picker. Does not change the Convex schema type. |
| required | boolean | false | When false, the field is wrapped in v.optional(…) in the schema and the form allows leaving it empty. |
| label | string | "" | Display label in the admin panel. Inferred from the field key by defineCollection when left empty. |
| admin.hidden | boolean | false | Hides the field from the admin edit form entirely. |
| admin.readOnly | boolean | false | Renders the picker as non-interactive — value is visible but cannot be changed. |
| admin.position | "main" \| "sidebar" | "main" | Column in the admin edit layout where this field appears. |
| admin.width | string | "100%" | CSS width of the field in the edit form (e.g. "50%", "240px"). Meaningful as a child of row(); below a narrow form column every field renders full width regardless. |
| admin.cellAlignment | "left" \| "center" \| "right" | "left" | Horizontal alignment of the value in the list-view table cell. |
| admin.components.preview | Component | — | Per-field override for the relationship preview renderer — the only supported location for a custom preview. |
| admin.condition | (props: { data, siblingData }) => boolean | — | Shows the field only while this returns true. A throwing condition counts as met. Hidden fields skip required/validate on the server too. See the Conditional fields & masks guide. |
Schema output
Section titled “Schema output”// Single reference (hasMany: false, required: true)author: v.array(v.id("authors"))
// Single reference (hasMany: false, required: false — default)author: v.optional(v.array(v.id("authors")))
// Multi-reference (hasMany: true, required: false)tags: v.optional(v.array(v.id("tags")))Auto-generated indexes:
// For every relationship field — added automatically, no config needed.index("by_author", ["author"])
// On the *target* collection — added when another collection links to it// and useAsTitle is not a system field.searchIndex("search_title", { searchField: "title", filterFields: [] })import { defineCollection, relationship, text } from "@vexcms/core";
// Single reference — stores the author's Idconst posts = defineCollection({ slug: "posts", fields: { title: text({ required: true }), author: relationship({ collection: { slug: "authors" } }), },});
// Multi-reference — stores an array of tag Idsconst articles = defineCollection({ slug: "articles", fields: { title: text({ required: true }), tags: relationship({ collection: { slug: "tags" }, hasMany: true, required: false, }), },});Custom preview component
Section titled “Custom preview component”By default the picker and list table show the target document’s useAsTitle
field. Override the renderer via admin.components.preview on the
relationship() field itself — this is the only supported override location.
A target collection cannot set its own default preview: CollectionConfig’s admin shape has
no components.preview slot at all — only the relationship() field’s own admin carries
one, so a per-field override is the only supported override location.
// Field-level override — only applies to the `author` field on `posts`const posts = defineCollection({ slug: "posts", fields: { author: relationship({ collection: { slug: "authors" }, admin: { components: { preview: CompactAuthorPreview }, }, }), },});Admin UI
Section titled “Admin UI”The relationship field renders as a searchable combobox picker in the edit form,
with a button labeled “Select <label>” (the field’s own label) that opens a
picker dialog on its Select tab. Typing into the picker searches the target
collection’s useAsTitle field via the auto-generated search index. The
picker finds existing relationships by field, so re-opening the picker on an
already-filled field shows the current selection. When hasMany: false,
selecting a result replaces the current value; when hasMany: true, results
are added to the selection list and can be removed individually. In the
list-view table, each referenced document row shows the target collection’s
admin.icon beside its preview component (or title field as a fallback).