Blocks
Block Schema Generation
createVextroBlocksSchema() generates Convex table definitions and a block type registry from your block definitions. It produces the tables, validators, and metadata that power the block system at runtime.
Built-in Layout injection
createVextroBlocksSchema() always injects a built-in Layout block definition:
- slug:
vextroLayout - table:
vextroLayoutBlocks
You do not define this block manually. It is included automatically in:
definitionstablesblocksFieldcomponentConfig
The injected Layout block powers nested block composition in the admin editor and includes built-in layout options. The editor currently supports stack, flex, and grid body modes with keyboard-first workflows for adding, reordering, and validating nested children. Grid mode lets authors configure columns (1-12) and assign nested blocks to specific cells.
When editors switch between layout modes, Vextro retains dormant mode-specific values instead of clearing them. This allows safe mode toggling during content authoring.
Reserved identifiers
vextroLayout and vextroLayoutBlocks are reserved. Do not use them for app-defined block slugs, blockBehavior slugs, or schema table names when blocks are enabled.
Basic usage
Pass an array of block definitions and spread the returned tables into your Convex schema.
import { createVextroBlocksSchema } from "vextro";
import { defineSchema } from "convex/server";
const { tables, blocksField, registry } = createVextroBlocksSchema({
definitions: [hero, cta, testimonial],
});
export default defineSchema({
...tables,
// your other tables
}); Return value
| Property | Type | Description |
|---|---|---|
definitions | VextroBlockDefinition[] | The resolved block definitions with defaults applied |
registry | VextroBlockRegistry | Registry object containing definitions, statusValues, statusValidator, tableNames, tableNameValidator, blockIdValidator, and optional commonOptionFields/commonOptionGroups |
blocksField | v.array(v.union(...)) | A Convex validator for block reference arrays |
tables | Record<string, TableDefinition> | Table definitions to spread into defineSchema |
presetTable | TableDefinition | undefined | Table for block presets, if commonOptions.presets is configured |
componentConfig | object | Configuration passed to the Vextro Convex component for block routing |
mainAppBlockDefs | VextroBlockDefinition[] | Block definitions that live in the main Convex namespace (not the component) |
Arguments
createVextroBlocksSchema() accepts the following top-level options:
| Option | Type | Default | Description |
|---|---|---|---|
definitions | VextroBlockDefinition[] | required | Block definitions to generate tables and registry for |
includeOrder | boolean | true | Include an order field on each block reference |
nameIndexField | string | "name" | Field name to create a by_<field> index on every block table. Set to "" to skip. |
commonOptions | VextroCommonOptionsInput | -- | Shared options applied to every block (spacing, appearance, etc.) |
mainAppBlocks | Array<{ slug, tableName, label, picker? }> | -- | App-level block references that live in the main Convex namespace rather than the Vextro component |
mainAppBlocks
When your application defines its own block tables outside of Vextro (e.g., blocks backed by regular collections), you can mix them into the same blocks field alongside Vextro component blocks. Pass mainAppBlocks to register these app-level block types so the schema validator and admin picker include them.
import { createVextroBlocksSchema } from "vextro";
const { tables, blocksField } = createVextroBlocksSchema({
definitions: [hero, cta],
mainAppBlocks: [
{
slug: "productCard",
tableName: "productCards",
label: "Product Card",
picker: { group: "Commerce", icon: "ShoppingCart" },
},
],
}); App-level blocks use componentSource: "main" in the stored reference array, while Vextro component blocks use componentSource: "vextroBlocks". Slugs must be unique across both sources.
nameIndexField
By default, every block table gets a by_name index on the name field. If your blocks use a different field for their display name, set nameIndexField to that field name. Pass an empty string to skip index generation.
const { tables } = createVextroBlocksSchema({
definitions: [hero, cta],
nameIndexField: "title", // creates by_title index instead of by_name
}); Common options
All blocks can share a set of fields that appear alongside block-specific fields. Configure these with commonOptions to add spacing, visibility, or other cross-cutting concerns.
const { tables } = createVextroBlocksSchema({
definitions: [hero, cta],
commonOptions: {
fields: {
spacing: f.select({
options: ["none", "sm", "md", "lg"],
defaultValue: "md",
}),
visible: f.checkbox({ defaultValue: true }),
},
groups: [
{ label: "Spacing", fields: ["spacing"] },
{ label: "Visibility", fields: ["visible"] },
],
presets: {
tableName: "blockPresets",
},
},
}); Common option fields are appended to every block's edit form under the configured group labels. The optional presets config creates a table for saving and reusing common option combinations.
Responsive overrides
Common options can be made responsive so editors can override values per breakpoint. Add a responsive config to commonOptions specifying which fields allow per-breakpoint overrides. The responsive property accepts a VextroResponsiveConfig object.
const { tables } = createVextroBlocksSchema({
definitions: [hero, cta],
commonOptions: {
fields: {
spacing: f.select({
options: ["none", "sm", "md", "lg"],
defaultValue: "md",
}),
alignment: f.select({
options: ["left", "center", "right"],
defaultValue: "left",
}),
visible: f.checkbox({ defaultValue: true }),
},
responsive: {
breakpoints: ["base", "sm", "md", "lg", "xl"],
fields: ["spacing", "alignment"],
},
},
}); VextroResponsiveConfig
| Option | Type | Default | Description |
|---|---|---|---|
breakpoints | VextroResponsiveBreakpoint[] | ["base", "sm", "md", "lg", "xl"] | Which breakpoints to show in the options panel. Values: "base", "sm", "md", "lg", "xl". |
fields | string[] | required | Common option field names that can be overridden per breakpoint. |
The five available breakpoints ("base", "sm", "md", "lg", "xl") follow standard responsive design conventions. You can include a subset if your design system uses fewer breakpoints.
When responsive is configured, Vextro auto-injects a _responsiveOverrides system field on every block table. The field stores per-breakpoint overrides as { [breakpoint]: { [fieldName]: value } }.
Fields listed in responsive.fields show breakpoint tabs in the block options panel. Non-listed fields always use their base value regardless of the active breakpoint.
Consuming app rendering
The _responsiveOverrides data is stored on the block document. Your frontend rendering code is responsible for reading overrides and applying the correct value per breakpoint (e.g., using CSS media queries or a responsive utility). Vextro stores the data — your app applies it.
Using createVextroSchema
For projects that use collections, globals, and blocks together, createVextroSchema provides a single entry point that handles all schema generation. Import it from vextro/convex/schema:
import { createVextroSchema } from "vextro/convex/schema";
const vextro = createVextroSchema({
collections: [pages, posts],
globals: [mainMenu],
blocks: {
definitions: [hero, cta],
commonOptions: { /* ... */ },
},
});
export default defineSchema({ ...vextro.tables }); This is equivalent to calling createVextroBlocksSchema, createVextroCollectionsSchema, and createVextroGlobalsSchema separately and merging the results.
Block field validator
The blocksField return value is a v.array(v.union(...)) Convex validator built from all registered block types. Each union member validates a BlockEntry -- either an inline entry or a reference entry -- for a specific block type. Use it when you need to define a blocks column on a table manually rather than through f.blocks().
Reference entries follow the { kind: 'reference', blockType, blockId, order, componentSource } shape. Inline entries follow { kind: 'inline', blockType, blockId, order, data } where data contains the embedded field values. Legacy entries without a kind field are treated as reference entries for backward compatibility -- use normalizeBlockEntry() from vextro to apply this coercion at runtime.