Fields

Relationship Field

The relationship field (created via f.id()) stores a reference to one or more documents in another Convex collection. It renders a searchable picker in the admin UI that lets editors browse and select related documents. Use this field for author references, category assignments, tag lists, or any cross-collection link.

The underlying Convex validator is v.id(tableName) for single relationships or v.array(v.id(tableName)) for multiple, wrapped in v.optional() unless marked as required.

Config options

OptionTypeRequiredDefaultDescription
tableNamestring*The Convex table to reference (first positional argument)
requiredbooleanfalseMakes the field required in the schema and admin UI
readOnlybooleanfalseRenders the picker as non-editable
placeholderstringPlaceholder text for the search input
labelstring | functionField nameCustom label for the admin UI
descriptionstringHelp text displayed below the field label
multiplebooleanfalseAllow selecting multiple documents
hasManybooleanfalseAlias for multiple
minItemsnumberMinimum number of selections (only for multiple)
maxItemsnumberMaximum number of selections (only for multiple)
pickerFilterFieldConditionFilter documents shown in the picker
pickerSort{ field: string; direction?: "asc" | "desc" }Sort order for picker results
displayAsstring | ((doc) => string)Custom display renderer for relationship values in list views and editors. When a string, uses that field name from the related document. When a function, receives the related document and returns a display string (functions are registered client-side via setRelationshipRenderers()).
scopeFilterbooleanfalseAuto-filter relationship options by the active scope in multi-tenant setups. When true and the target collection is scoped, the picker only shows options matching the current scope value.
conditionFieldConditionCondition for showing or hiding this field
sidebarbooleanfalsePlace this field in the document sidebar
listColumnbooleanfalseShow as a default column in list views

Example usage

import { f, defineVextroCollection } from "vextro";

export const posts = defineVextroCollection({
  slug: "posts",
  label: "Posts",
  collectionType: "content",
  tableName: "posts",
  fields: {
    title: f.text({ required: true }),
    author: f.id("users", {
      required: true,
      label: "Author",
      listColumn: true,
    }),
    category: f.id("categories", {
      required: true,
      pickerFilter: { field: "status", equals: "active" },
      pickerSort: { field: "name", direction: "asc" },
    }),
    tags: f.id("tags", {
      hasMany: true,
      minItems: 1,
      maxItems: 10,
      label: "Tags",
      description: "Select between 1 and 10 tags",
    }),
    relatedPosts: f.id("posts", {
      multiple: true,
      maxItems: 5,
      label: "Related Posts",
      pickerFilter: { field: "status", equals: "published" },
    }),
  },
});

Admin options

The relationship picker displays a searchable list of documents from the target collection. The picker uses the target collection's useAsTitle field for display labels. When pickerFilter is set, the picker only shows documents matching the condition, which is useful for filtering out draft or inactive records.

For multiple relationships, selected items appear as an ordered list that supports drag-to-reorder. The minItems and maxItems constraints are validated on save.

Custom display rendering

Use displayAs to control how relationship values appear in list views and the editor. Pass a field name string to use that field from the related document, or a function for custom formatting:

// Use a specific field for display
author: f.id("users", {
  displayAs: "email",
})

// Custom function (registered client-side)
author: f.id("users", {
  displayAs: (doc) => `${doc.firstName} ${doc.lastName}`,
})

Client-side functions

Function values for displayAs are not serializable. They are stored as a "__custom" marker in the admin config and must be registered client-side using setRelationshipRenderers(). String values work without any client-side setup.

Scope filtering

In multi-tenant setups where collections use scope configuration, enable scopeFilter to automatically limit the picker to documents matching the active scope:

category: f.id("categories", {
  scopeFilter: true,
  label: "Category",
  description: "Only shows categories from the current tenant",
})

When scopeFilter is true and the target collection has a scope field, the relationship picker filters options to those matching the active scope value. This prevents editors from accidentally referencing documents from other tenants or regions.

Conditional display

reviewer: f.id("users", {
  label: "Reviewer",
  condition: { field: "status", equals: "in_review" },
  description: "Assign a reviewer for this document",
})
Previous
Point
Next
Upload