Blocks
Collection Opt-In
Collections can opt in as block types using blockBehavior, making them available in block pickers alongside component-hosted blocks while keeping their tables in the main Convex namespace.
Basic opt-in
Set blockBehavior: true on any collection to register it as a block type. The collection's slug and label are used as the block slug and picker label by default.
import { f, defineVextroCollection } from "vextro";
export const heroSections = defineVextroCollection({
label: "Hero Sections",
tableName: "heroSections",
blockBehavior: true,
fields: {
heading: f.text({ required: true }),
subheading: f.textarea(),
backgroundImage: f.image({ relationTo: "media" }),
},
}); Configuration options
| Option | Type | Default | Description |
|---|---|---|---|
blockBehavior | boolean | VextroBlockBehaviorConfig | -- | Opt this collection in as a block type |
blockBehavior.slug | string | collection slug | Override the block slug |
blockBehavior.label | string | collection label | Override the block label in the picker |
blockBehavior.picker | VextroBlockPicker | collection picker | Picker group, thumbnail, and icon |
Pass an object instead of true to customize how the collection appears in the block picker:
export const testimonials = defineVextroCollection({
label: "Testimonials",
tableName: "testimonials",
blockBehavior: {
slug: "testimonial",
label: "Testimonial Block",
picker: { group: "Social Proof" },
},
fields: {
quote: f.textarea({ required: true }),
author: f.text({ required: true }),
},
}); Mixing block sources
A single f.blocks() field can accept both component-hosted block definitions and collection-based blocks. Vextro routes operations to the correct storage location automatically.
import { hero, cta } from "./blocks";
import { heroSections } from "./collections";
export const pages = defineVextroCollection({
label: "Pages",
collectionType: "content",
tableName: "pages",
fields: {
title: f.text({ required: true }),
content: f.blocks([hero, cta, heroSections]),
},
}); The stored data uses the componentSource discriminant to distinguish between block sources:
[
{ "blockType": "hero", "blockId": "abc123", "componentSource": "vextroBlocks", "order": 0 },
{ "blockType": "heroSections", "blockId": "heroSections:def456", "componentSource": "main", "order": 1 }
] Component-hosted blocks use "vextroBlocks" as the source, while collection-based blocks use "main".
Auto-collection in schema
Collections with blockBehavior are automatically collected by createVextroSchema() and registered as mainAppBlocks. You do not need to pass them separately -- the schema generator discovers them from your collection definitions.
Benefits
- Appear in the block picker alongside component-hosted blocks
- Support full
usedInreference tracking and multi-use confirmation - Maintain full
v.id()type safety since tables live in the main Convex namespace - Visible in the main admin dashboard as standalone collections
- Editable both as blocks within a parent document and as independent collection entries
When to use
Use collection opt-in (blockBehavior) when:
- The block data is also useful as a standalone collection (e.g., testimonials, FAQs, team members)
- You need direct
v.id()references to block records from other tables - You want blocks visible in the main Convex dashboard for independent querying
- You are migrating existing collections to also serve as blocks
Use component-hosted blocks (defineVextroBlock) when:
- The block is purely structural and only makes sense inside a parent document (e.g., hero sections, CTAs, layout containers)
- You want to keep block tables isolated from your main Convex namespace
- You do not need to query or list blocks independently