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

OptionTypeDefaultDescription
collectionstringrequiredTarget collection to query
onstringrequiredField (or dot-path) in the target collection referencing this document
labelstringcollection nameDisplay label
displayFieldstringfirst text fieldField to show for each related item
limitnumber50Maximum related items to show
indexstring—Convex index to use for the query
throughFieldstring—Junction table field for many-to-many
throughCollectionstring—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.

Previous
Blocks
Next
Virtual