Fields
Join Field
The join field creates a read-only reverse relationship. Instead of storing a reference, it queries another collection for documents that point back to the current one. Join fields are virtual -- they store no data and are computed at query time.
Config Options
| Option | Type | Default | Description |
|---|---|---|---|
collection | string | required | Target collection to query |
on | string | required | Field (or dot-path) in the target collection referencing this document |
label | string | collection name | Display label |
displayField | string | first text field | Field to show for each related item |
limit | number | 50 | Maximum related items to show |
index | string | — | Convex index to use for the query |
throughField | string | — | Junction table field for many-to-many |
throughCollection | string | — | Final collection for many-to-many |
Example Usage
import { f } from "vextro";
const fields = {
name: f.text({ required: true }),
email: f.email({ required: true }),
// Show all posts authored by this user
posts: f.join({
collection: "posts",
on: "author",
label: "Published Posts",
displayField: "title",
limit: 20,
}),
}; Nested Field Lookups (Dot-Paths)
The on option supports dot-separated paths for referencing ID fields inside f.group() or f.array() structures:
// Target collection (stores) has:
// manager: f.group({ fields: { employee: f.id("employees"), hubspotId: f.text() } })
// designers: f.array({ fields: { employee: f.id("employees"), role: f.text() } })
// On the employees collection:
const fields = {
name: f.text({ required: true }),
// Group dot-path: finds stores where manager.employee === this employee's ID
managerAt: f.join({
collection: "stores",
on: "manager.employee",
label: "Manages",
displayField: "name",
}),
// Array dot-path: finds stores where any designers[].employee === this employee's ID
designerAt: f.join({
collection: "stores",
on: "designers.employee",
label: "Designer at",
displayField: "name",
}),
}; Dot-paths work at any nesting depth (e.g., config.team.leadId). When an array is encountered in the path, every element is checked for a match.
Performance
Nested dot-path joins require a full table scan with filtering since Convex does not support indexes on nested fields. For top-level on fields, provide an index option to use an indexed query instead.
Many-to-Many Relationships
For many-to-many via a junction table, use throughField and throughCollection:
categories: f.join({
collection: "post_categories",
on: "postId",
throughField: "categoryId",
throughCollection: "categories",
}) This queries post_categories where postId matches, then resolves each categoryId from categories.
Admin Behavior
Join fields are always read-only. They display a list of related documents with links to their edit pages. For large collections, specify an index to keep the join query efficient.
No stored data
Join fields are virtual. They do not create a column in the Convex table and are excluded from import/export operations.