LLM Reference

LLM Reference: Collections, Globals & Blocks

defineVextroCollection

Defines a Vextro collection with type-safe field builders. Collections are the primary content containers in Vextro, mapping to Convex tables with admin UI metadata, access control, hooks, and schema validators.

VextroCollectionInput type

FieldTypeDefaultDescription
slugstringtableNameURL-safe identifier for the collection
labelstringrequiredDisplay label in admin UI
descriptionstring—Description shown in admin UI
groupstring—Sidebar organization group
collectionTypestring—Semantic label for sidebar grouping. Built-in values ("content", "config", "system") map to trait presets when no explicit traits provided. Any string accepted.
traitsPartial<VextroCollectionTraits>—{ statusWorkflow?, autoTimestamps?, auditFields?, versioning?, softDelete? }. Overrides trait preset from collectionType. Defaults: statusWorkflow: false, autoTimestamps: true, auditFields: true, versioning: false, softDelete: false.
tableNamestringrequiredConvex table name
fieldsVextroFieldsInputrequiredField definitions using f.* builders or raw Convex validators
statusConfigVextroStatusConfig—field?: string (default: "status"), values?: string[] (default: ["draft", "published", "scheduled", "trashed"]), index?: string (default: "by_status")
fieldNamesVextroCollectionFieldNames—Override default field names: status?, updatedAt?, updatedBy?, createdBy?, slug?
scopeVextroCollectionScope—{ field: string, type?: string } for multi-tenant content
listConfigVextroListConfig—columns?: string[], searchableFields?: string[], defaultSort?: string, defaultSortDirection?: "asc" | "desc", defaultLayout?: "list" | "grid", folderFields?: string[]
accessVextroAccessConfig—{ read?, create?, update?, delete? } permission slugs
pickerVextroCollectionPicker—{ group?, thumbnail?, icon? } for picker dialogs
versionsVextroVersioningConfig—{ enabled: boolean, maxVersions?: number } (default maxVersions: 25)
useAsTitlestring—Field name to use as document title in admin UI. Must be a text, number, or richText field.
uploadboolean | UploadConfig—Enable uploads. true for defaults, or pass UploadConfig with mimeTypes?, maxFileSize?, imageSizes?, focalPoint?, crop?, formatOptions?, resizeOptions?, adminThumbnail?, bulkUpload?, displayPreview?, filesRequiredOnCreate?, pasteURL?, storageAdapter?, prefix?.
previewVextroPreviewConfig—{ url: string, enabled?: boolean }. URL template with {field} placeholders, e.g. "/blog/{slug}".
workflowWorkflowConfig—{ enabled: boolean, stages: WorkflowStageDefinition[] }. Each stage has name, label, color?, requiredRole?, autoTransition?.
localizationVextroLocalizationConfig—{ enabled: boolean, locales: string[], defaultLocale: string, localizedFields?: string[] }
hooksCollectionHooks—{ beforeChange?, afterChange?, beforeDelete?, afterDelete?, beforeRead?, afterRead? } — each is an array of hook functions
sidebarConfigVextroSidebarConfig—{ sectionOrder?: string[] } — controls sidebar section display order
sidebarbooleantrueWhether this collection appears in the admin sidebar
hierarchyVextroHierarchyConfig—{ parentField?: string, maxDepth?: number } for nested document trees
blockBehaviorboolean | VextroBlockBehaviorConfig—Opt this collection in as a block type. true for defaults, or { slug?, label?, picker? } to customize. Collections with blockBehavior appear in block pickers and can be passed directly to f.blocks().
autoSaveboolean | VextroAutoSaveConfig—Auto-save behavior for the document editor. true enables with defaults (3000ms debounce). false disables. Object form: { enabled?: boolean, debounceMs?: number }. Collection-level value overrides the project-level autoSave default in VextroConfig.
indexesVextroIndexDefinition[][]Additional indexes beyond the auto-generated ones. Each has { name: string, fields: [string, ...string[]] }.

Auto-injected fields (trait-dependent)

  • updatedAt (v.number()) — injected when autoTimestamps trait is true (default)
  • updatedBy (v.optional(v.string())) — injected when auditFields trait is true (default)
  • createdBy (v.optional(v.string())) — injected when auditFields trait is true (default)
  • status (union of status values) — injected when statusWorkflow trait is true

Auto-injected indexes

  • by_status — when statusWorkflow trait is true, indexing the status field
  • by_slug — when fieldNames.slug is set
  • by_{scope.field} — when scope is configured

User-defined indexes with the same name override the auto-generated ones.

Upload auto-injected fields

When upload is enabled, these fields are auto-injected: filename, mimeType, filesize, fileId, width, height. When focalPoint is not explicitly false, focalX and focalY are also added.

Return value

Returns a VextroCollectionDefinition with:

  • .table — Convex TableDefinition for use in defineSchema
  • .fields — Resolved Convex validators (user fields + auto fields)
  • .adminFields — Admin UI field configurations
  • .layoutConfig — Tabs, rows, collapsibles layout metadata
  • .defaults — Resolved field names and status config
  • .fieldHooks — Field-level hooks keyed by field path, extracted from inline hooks options on each f.*() builder call. See FieldHooks for the hook type.
  • All input properties passed through

Example

import { f, defineVextroCollection } from "vextro";

export const posts = defineVextroCollection({
  label: "Posts",
  collectionType: "content",
  tableName: "posts",
  useAsTitle: "title",
  fields: {
    title: f.text({ required: true, listColumn: true, searchable: true }),
    slug: f.slug({ sourceField: "title", required: true }),
    body: f.richText(),
    author: f.id("users"),
  },
  versions: { enabled: true },
  access: {
    read: "content:read",
    create: "content:create",
    update: "content:update",
    delete: "content:delete",
  },
  listConfig: {
    columns: ["title", "status", "author"],
    searchableFields: ["title", "slug"],
    defaultSort: "updatedAt",
    defaultSortDirection: "desc",
  },
});

// In schema.ts:
import { defineSchema } from "convex/server";
export default defineSchema({
  [posts.tableName]: posts.table,
});

defineVextroGlobal

Defines a Vextro global (singleton) using field builders. Globals are single-document tables for site-wide settings and configuration. Unlike collections, globals have no status workflow, no list view, no slug field, no routing, no scope, and no versioning.

Full options

FieldTypeDefaultDescription
slugstringrequiredURL-safe identifier
labelstringrequiredDisplay label in admin UI
descriptionstring—Description shown in admin UI
groupstring—Sidebar organization group
tableNamestringrequiredConvex table name
fieldsVextroFieldsInputrequiredField definitions using f.* builders
accessVextroGlobalAccessConfig—{ read?, update? } permission slugs
hooksGlobalHooks—{ beforeChange?, afterChange?, beforeRead?, afterRead? } — each is an array of hook functions
sidebarConfigVextroGlobalSidebarConfig—{ sectionOrder?: string[] }

Auto-injected fields

  • updatedAt (v.number())
  • updatedBy (v.optional(v.string()))

No status field, no createdBy, no slug routing, no versioning, no scope.

Return value

Returns a VextroGlobalDefinition with:

  • .table — Convex TableDefinition for use in defineSchema (no indexes)
  • .fields — Resolved Convex validators
  • .adminFields — Admin UI field configurations
  • .layoutConfig — Layout metadata
  • .defaults — { updatedAtField: string, updatedByField: string }
  • .fieldHooks — Field-level hooks keyed by field name

Example

import { f, defineVextroGlobal } from "vextro";

export const company = defineVextroGlobal({
  slug: "company",
  label: "Company",
  description: "Company-wide settings and contact info.",
  group: "Settings",
  tableName: "globalCompany",
  fields: {
    tollFreeNumber: f.text(),
    email: f.email(),
  },
});

// In schema.ts:
import { defineSchema } from "convex/server";
export default defineSchema({
  ...collectionTables,
  [company.tableName]: company.table,
});

defineVextroBlock

Defines a Vextro block using the field builder syntax. Blocks are reusable content components that can be embedded in collections via f.blocks() fields.

VextroBlockInput type

FieldTypeDefaultDescription
slugstringtableNameURL-safe identifier for the block type
labelstringrequiredDisplay label in admin UI
tableNamestringrequiredConvex table name
fieldsVextroFieldsInputrequiredField definitions using f.* builders
statusValuesstring[]["draft", "published", "scheduled", "trashed"]Available status values
fieldNamesVextroBlockFieldNames—Override default field names: status?, updatedAt?, updatedBy?, usedIn?, thumbnail?
statusIndexstring"by_status"Index name for status queries
descriptionstring—Description shown in admin UI
groupstring"Blocks"Sidebar organization group
collectionTypeVextroCollectionType"content"Collection type in admin
scopeVextroCollectionScope—{ field: string, type?: string } for multi-tenant content
listConfigVextroListConfig—List view configuration
accessVextroAccessConfig—{ read?, create?, update?, delete? } permission slugs
pickerVextroBlockPicker—{ group?, thumbnail?, icon? } for block picker dialog
optionsVextroFieldsInput—Block-specific options using f.* builders (rendered in Options panel)
sidebarbooleanfalseWhether this block collection appears in the admin sidebar
responsiveOptionsstring[]—Which block-specific option field names can be overridden per breakpoint
childrenReadonlyArray<VextroBlockTypeSpecifier>—Allowed child block types. Auto-injects a children blocks field on the block table.
maxDepthnumber3Maximum nesting depth. Only meaningful when children is set.

Auto-injected fields

  • status — union of status values (default: "draft" \| "published" \| "scheduled" \| "trashed")
  • updatedAt (v.number())
  • updatedBy (v.optional(v.string()))
  • usedIn (v.optional(v.array(v.object({ collection, tableName, documentId, field? })))) — array of usage references tracking where the block is embedded
  • thumbnail (v.optional(v.string()))
  • _options (v.optional(v.any())) — injected when options is provided
  • _blockOptions (v.optional(v.any())) — injected when commonOptions is provided to createVextroBlocksSchema
  • _stylePreset (v.optional(v.id(presetTableName))) — injected when commonOptions.presets is configured
  • children (v.optional(v.array(v.union(...)))) — injected when children is set on the block definition; stores nested block references

Example

import { f, defineVextroBlock } from "vextro";

export const cta = defineVextroBlock({
  label: "CTA",
  tableName: "ctaBlocks",
  fields: {
    name: f.text({ required: true }),
    heading: f.text(),
    body: f.textarea(),
    buttonLabel: f.text(),
    buttonUrl: f.url(),
  },
  options: {
    alignment: f.select({ options: ["left", "center", "right"] }),
    background: f.select({ options: ["default", "dark", "gradient"] }),
  },
  picker: { group: "Marketing", icon: "megaphone" },
});

blockRef()

Lightweight forward reference to a block type. Use when defining blocks that reference each other (circular dependencies) or when you only need the block’s identity for defineVextroBlocksField() or f.blocks().

function blockRef(input: {
  slug: string;
  label: string;
  tableName: string;
  picker?: VextroBlockPicker;
}): VextroBlockTypeRef;

Returns a branded VextroBlockTypeRef that is accepted anywhere a VextroBlockTypeSpecifier is expected (i.e., defineVextroBlocksField and createVextroBlocksSchema).

Example

import { blockRef } from "vextro";

// Use when the full block definition isn't available yet
const ctaRef = blockRef({
  slug: "ctaBlocks",
  label: "CTA",
  tableName: "ctaBlocks",
  picker: { group: "Marketing", icon: "megaphone" },
});

createVextroBlocksSchema

The main helper that creates all block-related schema artifacts from an array of block definitions. Handles definition resolution, common options injection, registry creation, field validator generation, and table creation.

function createVextroBlocksSchema({
  definitions,      // array of VextroBlockDefinitionInput or VextroBlockDefinition
  includeOrder?,    // default: true -- include order field in block refs
  nameIndexField?,  // default: "name" -- field to create by_name index on
  commonOptions?,   // shared options injected on every block
  mainAppBlocks?,   // app-level block routing for mixing component blocks with app blocks
}): {
  definitions: VextroBlockDefinition[];   // resolved block definitions
  registry: object;                       // block registry with validators and metadata
  blocksField: Validator;                 // validator for use in collection field definitions
  tables: Record<string, TableDefinition>; // spread into defineSchema
  presetTable?: {                         // only present when commonOptions.presets is configured
    tableName: string;
    table: TableDefinition;
  };
  mainAppBlockDefs: Array<{ slug: string; tableName: string; label: string; picker?: VextroBlockPicker }>;
};

Args detail:

ArgTypeDefaultDescription
definitionsArray<VextroBlockDefinitionInput | VextroBlockDefinition>requiredBlock definitions to process
includeOrderbooleantrueInclude order field in block ref validators
nameIndexFieldstring"name"Field name used to create by_{nameIndexField} index on each block table
commonOptionsVextroCommonOptionsInput—Shared options injected on every block
mainAppBlocksArray<{ slug, tableName, label, picker? }>—App-level block definitions for mixing component blocks with app-hosted blocks

Return value detail:

FieldTypeDescription
definitionsVextroBlockDefinition[]Resolved block definitions
registryobjectBlock registry with validators, admin fields, and metadata
blocksFieldValidatorValidator for use in collection field definitions
tablesRecord<string, TableDefinition>Block tables to spread into defineSchema
presetTable{ tableName: string; table: TableDefinition } | undefinedPresent only when commonOptions.presets is configured
mainAppBlockDefsArray<{ slug, tableName, label, picker? }>Resolved app-level block definitions passed through from mainAppBlocks

VextroCommonOptionsInput

Shared options applied to every block. When provided, every block gets a _blockOptions field and optionally a _stylePreset reference field.

type VextroCommonOptionsInput = {
  /** Common option field definitions using the field builder API */
  fields: VextroFieldsInput;
  /** Optional UI grouping for the options panel */
  groups?: VextroCommonOptionsGroup[];
  /** Optional style presets table configuration */
  presets?: { tableName: string };
  /** Optional responsive overrides for eligible common option fields */
  responsive?: VextroResponsiveConfig;
};

type VextroCommonOptionsGroup = {
  /** Group section label */
  label: string;
  /** Field names in this group */
  fields: string[];
  /** Start collapsed (default: false) */
  collapsed?: boolean;
};

type VextroResponsiveConfig = {
  /** Which breakpoints to show (defaults to all five) */
  breakpoints?: VextroResponsiveBreakpoint[];
  /** Which common option field names are responsive-eligible */
  fields: string[];
};

When commonOptions.responsive is provided, eligible common option fields can have per-breakpoint overrides. Block-level responsiveOptions on VextroBlockInput controls which block-specific option fields are responsive-eligible independently of the common options.

When commonOptions.presets is configured, the return value includes a presetTable with name, slug, and all common option fields plus updatedAt/updatedBy. The presets table gets by_slug and by_name indexes.

Example

import { f, defineVextroBlock, createVextroBlocksSchema } from "vextro";
import { defineSchema } from "convex/server";

const hero = defineVextroBlock({
  label: "Hero",
  tableName: "heroBlocks",
  fields: {
    name: f.text({ required: true }),
    heading: f.text({ required: true }),
    subheading: f.textarea(),
    image: f.upload("media"),
  },
});

const cta = defineVextroBlock({
  label: "CTA",
  tableName: "ctaBlocks",
  fields: {
    name: f.text({ required: true }),
    heading: f.text(),
    body: f.textarea(),
  },
});

const { blocksField, tables, definitions, presetTable } = createVextroBlocksSchema({
  definitions: [hero, cta],
  commonOptions: {
    fields: {
      margin: f.select({ options: ["sm", "md", "lg"], label: "Margin" }),
      background: f.select({
        options: ["default", "light", "dark"],
        label: "Background",
      }),
      breakout: f.checkbox({ label: "Breakout" }),
    },
    groups: [
      { label: "Spacing", fields: ["margin"] },
      { label: "Appearance", fields: ["background"] },
      { label: "Advanced", fields: ["breakout"], collapsed: true },
    ],
    presets: { tableName: "blockStylePresets" },
  },
});

// Use blocksField in a collection:
export const pages = defineVextroCollection({
  label: "Pages",
  collectionType: "content",
  tableName: "pages",
  fields: {
    title: f.text({ required: true }),
    slug: f.slug({ sourceField: "title", required: true }),
    blocks: f.blocks(definitions),
  },
});

// In schema.ts:
export default defineSchema({
  [pages.tableName]: pages.table,
  ...tables,
  ...(presetTable ? { [presetTable.tableName]: presetTable.table } : {}),
});

Schema helper functions

defineVextroBlockTable

Creates a Convex table definition from a single block definition, including the status index.

function defineVextroBlockTable({
  definition,
}: {
  definition: VextroBlockDefinition<string>;
}): TableDefinition;

defineVextroBlocksField

Creates the blocks field validator from an array of block type specifiers. Each specifier can be a full VextroBlockDefinition or a VextroBlockTypeRef from blockRef().

function defineVextroBlocksField({
  definitions,     // array of VextroBlockDefinition or VextroBlockTypeRef
  includeOrder?,   // default: true
}: {
  definitions: VextroBlockTypeSpecifier[];
  includeOrder?: boolean;
}): Validator;

The returned validator is v.array(v.union(...)) where each union member is v.object({ blockType: v.literal(slug), blockId: v.id(tableName), order: v.number() }).

createVextroCollectionsSchema

Helper to resolve and collect table definitions from multiple collections.

function createVextroCollectionsSchema({
  collections,  // array of VextroCollectionInput or VextroCollectionDefinition
}: {
  collections: Array<VextroCollectionInput | VextroCollectionDefinition>;
}): {
  definitions: VextroCollectionDefinition[];
  tables: Record<string, TableDefinition>;
};

Spreading into defineSchema

The standard pattern for combining collections, globals, and blocks into a single schema:

import { defineSchema } from "convex/server";

export default defineSchema({
  // Collections
  [posts.tableName]: posts.table,
  [pages.tableName]: pages.table,

  // Globals
  [company.tableName]: company.table,

  // Blocks (spread from createVextroBlocksSchema)
  ...blockTables,

  // Preset table (when using commonOptions.presets)
  ...(presetTable ? { [presetTable.tableName]: presetTable.table } : {}),
});

Hook types reference

CollectionHooks

type CollectionHooks = {
  beforeChange?: Array<BeforeChangeHook>;   // runs before create/update, return modified data or void
  afterChange?: Array<AfterChangeHook>;     // runs after create/update, return patch or void
  beforeDelete?: Array<BeforeDeleteHook>;   // runs before delete, throw to abort
  afterDelete?: Array<AfterDeleteHook>;     // runs after delete, side effects only
  beforeRead?: Array<BeforeReadHook>;       // runs before output transform
  afterRead?: Array<AfterReadHook>;         // runs after field afterRead hooks and locale merge
};

GlobalHooks

type GlobalHooks = {
  beforeChange?: Array<GlobalBeforeChangeHook>;  // runs before global update
  afterChange?: Array<GlobalAfterChangeHook>;    // runs after global update
  beforeRead?: Array<GlobalBeforeReadHook>;      // runs before global read
  afterRead?: Array<GlobalAfterReadHook>;        // runs after global read
};

FieldHooks

Field-level hooks are defined inline via f.* builder options (e.g., f.text({ hooks: { ... } })). They are extracted during defineVextroCollection and stored on .fieldHooks — separately from the serializable admin config, because functions cannot be stored in Convex.

type FieldHooks = {
  beforeChange?: Array<FieldBeforeChangeHook>;     // transform field value before write
  afterRead?: Array<FieldAfterReadHook>;           // transform field value after read
  afterChange?: Array<FieldAfterChangeHook>;       // side effects after db write
  beforeDuplicate?: Array<FieldBeforeDuplicateHook>; // transform copied field value
};

The fieldHooks map on the collection definition (type Record<string, FieldHooks>) is keyed by field name and is used internally by the admin module’s mutation/query pipelines. It is not a top-level defineVextroCollection input — hooks must be declared inline on each f.*() builder.

Hook execution order

  • Reads: beforeRead -> locale merge -> field afterRead (per field) -> collection afterRead
  • Writes: field beforeChange -> collection beforeChange -> db write -> field afterChange -> collection afterChange
  • Deletes: beforeDelete -> db delete -> afterDelete

All hook arrays execute in order. A shared mutable HookContext (Record<string, unknown>) is passed through all hooks in a single invocation, enabling inter-hook communication.


Block deletion lifecycle

Deletion strategy

When a parent document is deleted, Vextro can either preserve or cascade-delete its embedded blocks. The strategy is set globally via the blocks.onDocumentDelete option in createVextroAdminModule (the schema module call):

// convex/vextro.config.ts
export const { schema, adminModule } = createVextroAdminModule({
  blocks: {
    definitions: [hero, cta, ...],
    onDocumentDelete: "delete-exclusive",  // "preserve" (default) | "delete-exclusive"
  },
  // ...
});
StrategyBehavior
"preserve" (default)Blocks are never deleted automatically. They remain in their table after the parent document is removed.
"delete-exclusive"Blocks that are only referenced by the deleted document (exclusive usage) are deleted. Blocks shared with other documents are left intact.

Exclusive block identification

Vextro tracks block usage in the auto-injected usedIn field on every block document:

// Auto-injected on every block table:
usedIn: v.optional(v.array(v.object({
  collection: v.string(),   // collection slug
  tableName: v.string(),    // parent table name
  documentId: v.string(),   // parent document id
  field: v.string(),        // block field name
})))

When delete-exclusive is active, a block is deleted only when its usedIn array contains no entries from other documents. Blocks referenced by multiple documents are preserved.

Orphan cleanup

Blocks with an empty or missing usedIn array are considered orphans. Vextro provides three admin module functions for managing them:

FunctionTypeDescription
getOrphanBlockStatsqueryReturns { totalOrphans, byBlockType: [{ tableName, label, count }] } — a count of orphaned blocks per block type
listOrphanedBlocksqueryReturns { items: [...] } — paginated list of orphan block records. Accepts optional tableName filter and limit.
deleteOrphanedBlocksmutationDeletes the specified block ids from a given table. Accepts { blockIds: string[], tableName: string }. Returns { deleted, skipped } — skips any block that has gained a usedIn reference since the query.

The built-in VextroOrphanBlocksPage uses these three functions together to provide an admin UI for reviewing and cleaning up orphaned blocks.

blockRef() — forward references for circular dependencies

When two block types reference each other (circular dependency), use blockRef() to break the cycle. blockRef() accepts only the identity fields needed to construct the validator and picker entry — it does not require the full VextroBlockDefinition:

import { blockRef, defineVextroBlock } from "vextro";

// Reference sectionBlock before it is defined
const sectionRef = blockRef({
  slug: "sectionBlocks",
  label: "Section",
  tableName: "sectionBlocks",
});

const cta = defineVextroBlock({
  label: "CTA",
  tableName: "ctaBlocks",
  fields: { heading: f.text() },
  children: [sectionRef],  // forward reference
});

const section = defineVextroBlock({
  label: "Section",
  tableName: "sectionBlocks",
  fields: { title: f.text() },
  children: [cta],  // circular — cta is already defined
});

blockRef() returns a VextroBlockTypeRef, which is accepted anywhere a VextroBlockTypeSpecifier is expected: f.blocks(), defineVextroBlocksField(), and children arrays on block definitions.

Previous
Fields & Builders