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
| Field | Type | Default | Description |
|---|---|---|---|
slug | string | tableName | URL-safe identifier for the collection |
label | string | required | Display label in admin UI |
description | string | — | Description shown in admin UI |
group | string | — | Sidebar organization group |
collectionType | string | — | Semantic label for sidebar grouping. Built-in values ("content", "config", "system") map to trait presets when no explicit traits provided. Any string accepted. |
traits | Partial<VextroCollectionTraits> | — | { statusWorkflow?, autoTimestamps?, auditFields?, versioning?, softDelete? }. Overrides trait preset from collectionType. Defaults: statusWorkflow: false, autoTimestamps: true, auditFields: true, versioning: false, softDelete: false. |
tableName | string | required | Convex table name |
fields | VextroFieldsInput | required | Field definitions using f.* builders or raw Convex validators |
statusConfig | VextroStatusConfig | — | field?: string (default: "status"), values?: string[] (default: ["draft", "published", "scheduled", "trashed"]), index?: string (default: "by_status") |
fieldNames | VextroCollectionFieldNames | — | Override default field names: status?, updatedAt?, updatedBy?, createdBy?, slug? |
scope | VextroCollectionScope | — | { field: string, type?: string } for multi-tenant content |
listConfig | VextroListConfig | — | columns?: string[], searchableFields?: string[], defaultSort?: string, defaultSortDirection?: "asc" | "desc", defaultLayout?: "list" | "grid", folderFields?: string[] |
access | VextroAccessConfig | — | { read?, create?, update?, delete? } permission slugs |
picker | VextroCollectionPicker | — | { group?, thumbnail?, icon? } for picker dialogs |
versions | VextroVersioningConfig | — | { enabled: boolean, maxVersions?: number } (default maxVersions: 25) |
useAsTitle | string | — | Field name to use as document title in admin UI. Must be a text, number, or richText field. |
upload | boolean | UploadConfig | — | Enable uploads. true for defaults, or pass UploadConfig with mimeTypes?, maxFileSize?, imageSizes?, focalPoint?, crop?, formatOptions?, resizeOptions?, adminThumbnail?, bulkUpload?, displayPreview?, filesRequiredOnCreate?, pasteURL?, storageAdapter?, prefix?. |
preview | VextroPreviewConfig | — | { url: string, enabled?: boolean }. URL template with {field} placeholders, e.g. "/blog/{slug}". |
workflow | WorkflowConfig | — | { enabled: boolean, stages: WorkflowStageDefinition[] }. Each stage has name, label, color?, requiredRole?, autoTransition?. |
localization | VextroLocalizationConfig | — | { enabled: boolean, locales: string[], defaultLocale: string, localizedFields?: string[] } |
hooks | CollectionHooks | — | { beforeChange?, afterChange?, beforeDelete?, afterDelete?, beforeRead?, afterRead? } — each is an array of hook functions |
sidebarConfig | VextroSidebarConfig | — | { sectionOrder?: string[] } — controls sidebar section display order |
sidebar | boolean | true | Whether this collection appears in the admin sidebar |
hierarchy | VextroHierarchyConfig | — | { parentField?: string, maxDepth?: number } for nested document trees |
blockBehavior | boolean | 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(). |
autoSave | boolean | 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. |
indexes | VextroIndexDefinition[] | [] | Additional indexes beyond the auto-generated ones. Each has { name: string, fields: [string, ...string[]] }. |
Auto-injected fields (trait-dependent)
updatedAt(v.number()) — injected whenautoTimestampstrait istrue(default)updatedBy(v.optional(v.string())) — injected whenauditFieldstrait istrue(default)createdBy(v.optional(v.string())) — injected whenauditFieldstrait istrue(default)status(union of status values) — injected whenstatusWorkflowtrait istrue
Auto-injected indexes
by_status— whenstatusWorkflowtrait istrue, indexing the status fieldby_slug— whenfieldNames.slugis setby_{scope.field}— whenscopeis 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— ConvexTableDefinitionfor use indefineSchema.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 inlinehooksoptions on eachf.*()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
| Field | Type | Default | Description |
|---|---|---|---|
slug | string | required | URL-safe identifier |
label | string | required | Display label in admin UI |
description | string | — | Description shown in admin UI |
group | string | — | Sidebar organization group |
tableName | string | required | Convex table name |
fields | VextroFieldsInput | required | Field definitions using f.* builders |
access | VextroGlobalAccessConfig | — | { read?, update? } permission slugs |
hooks | GlobalHooks | — | { beforeChange?, afterChange?, beforeRead?, afterRead? } — each is an array of hook functions |
sidebarConfig | VextroGlobalSidebarConfig | — | { 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— ConvexTableDefinitionfor use indefineSchema(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
| Field | Type | Default | Description |
|---|---|---|---|
slug | string | tableName | URL-safe identifier for the block type |
label | string | required | Display label in admin UI |
tableName | string | required | Convex table name |
fields | VextroFieldsInput | required | Field definitions using f.* builders |
statusValues | string[] | ["draft", "published", "scheduled", "trashed"] | Available status values |
fieldNames | VextroBlockFieldNames | — | Override default field names: status?, updatedAt?, updatedBy?, usedIn?, thumbnail? |
statusIndex | string | "by_status" | Index name for status queries |
description | string | — | Description shown in admin UI |
group | string | "Blocks" | Sidebar organization group |
collectionType | VextroCollectionType | "content" | Collection type in admin |
scope | VextroCollectionScope | — | { field: string, type?: string } for multi-tenant content |
listConfig | VextroListConfig | — | List view configuration |
access | VextroAccessConfig | — | { read?, create?, update?, delete? } permission slugs |
picker | VextroBlockPicker | — | { group?, thumbnail?, icon? } for block picker dialog |
options | VextroFieldsInput | — | Block-specific options using f.* builders (rendered in Options panel) |
sidebar | boolean | false | Whether this block collection appears in the admin sidebar |
responsiveOptions | string[] | — | Which block-specific option field names can be overridden per breakpoint |
children | ReadonlyArray<VextroBlockTypeSpecifier> | — | Allowed child block types. Auto-injects a children blocks field on the block table. |
maxDepth | number | 3 | Maximum 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 embeddedthumbnail(v.optional(v.string()))_options(v.optional(v.any())) — injected whenoptionsis provided_blockOptions(v.optional(v.any())) — injected whencommonOptionsis provided tocreateVextroBlocksSchema_stylePreset(v.optional(v.id(presetTableName))) — injected whencommonOptions.presetsis configuredchildren(v.optional(v.array(v.union(...)))) — injected whenchildrenis 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:
| Arg | Type | Default | Description |
|---|---|---|---|
definitions | Array<VextroBlockDefinitionInput | VextroBlockDefinition> | required | Block definitions to process |
includeOrder | boolean | true | Include order field in block ref validators |
nameIndexField | string | "name" | Field name used to create by_{nameIndexField} index on each block table |
commonOptions | VextroCommonOptionsInput | — | Shared options injected on every block |
mainAppBlocks | Array<{ slug, tableName, label, picker? }> | — | App-level block definitions for mixing component blocks with app-hosted blocks |
Return value detail:
| Field | Type | Description |
|---|---|---|
definitions | VextroBlockDefinition[] | Resolved block definitions |
registry | object | Block registry with validators, admin fields, and metadata |
blocksField | Validator | Validator for use in collection field definitions |
tables | Record<string, TableDefinition> | Block tables to spread into defineSchema |
presetTable | { tableName: string; table: TableDefinition } | undefined | Present only when commonOptions.presets is configured |
mainAppBlockDefs | Array<{ 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 -> fieldafterRead(per field) -> collectionafterRead - Writes: field
beforeChange-> collectionbeforeChange-> db write -> fieldafterChange-> collectionafterChange - 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"
},
// ...
});
| Strategy | Behavior |
|---|---|
"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:
| Function | Type | Description |
|---|---|---|
getOrphanBlockStats | query | Returns { totalOrphans, byBlockType: [{ tableName, label, count }] } — a count of orphaned blocks per block type |
listOrphanedBlocks | query | Returns { items: [...] } — paginated list of orphan block records. Accepts optional tableName filter and limit. |
deleteOrphanedBlocks | mutation | Deletes 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.