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

OptionTypeDefaultDescription
blockBehaviorboolean | VextroBlockBehaviorConfig--Opt this collection in as a block type
blockBehavior.slugstringcollection slugOverride the block slug
blockBehavior.labelstringcollection labelOverride the block label in the picker
blockBehavior.pickerVextroBlockPickercollection pickerPicker 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 usedIn reference 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
Previous
Reference Tracking