Tulip Logo IconTulip
Blocks

Registry and Schemas

Understand block definitions, registry inference, and create/select validation.

The registry is the center of a block feature. It keeps definitions as plain data while providing typed schemas and runtime helpers.

Block Definitions

export type BlockDefinition = {
  slug: string;
  label: string;
  description?: string;
  schema: z.ZodType;
};

The schema describes only data. Storage-owned fields live outside the schema.

const storedBlock = {
  id: "block_1",
  slug: "hero",
  index: 0,
  data: {
    titlePart1: "Make space",
    titlePart2: "for growth",
  },
};

Registry Output

const registry = createBlockRegistry([heroBlock, summaryBlock]);

The registry exposes:

  • definitions: the original definitions in order
  • createSchema: discriminated union for { slug, data }
  • selectSchema: discriminated union for { id, slug, index, data }
  • getDefinition(slug): runtime lookup for metadata/schema
  • patchData({ block, patch }): merge partial data and validate the result

Inferred Types

Use registry-level inference for app code.

type PageBlock = InferBlock<typeof pageBlocks>;
type PageBlockInput = InferBlockInput<typeof pageBlocks>;

Use definition-level inference inside individual block files.

type HeroBlock = InferBlockFromDefinition<typeof heroBlock>;
type HeroCreateInput = InferBlockInputFromDefinition<typeof heroBlock>;

Select Schema

Use selectSchema at boundaries that read persisted blocks.

const rows = await db.query.pageBlocks.findMany({ orderBy: asc(pageBlocksTable.index) });
const blocks = z.array(pageBlocks.selectSchema).parse(rows);

If old stored data no longer matches a block schema, parsing fails early before UI rendering.

Create Schema

Use createSchema in create mutations.

const inputSchema = z.object({
  pageId: z.string(),
  block: pageBlocks.createSchema,
});

The server still assigns id and index.

Patch Data

Inline editing usually sends partial field updates. patchData merges the patch into the existing data object and validates the full result against the block schema.

const patchedData = pageBlocks.patchData({
  block: existingBlock,
  patch: { titlePart1: "New title" },
});

if (!patchedData.success) {
  throw new Error("Unknown block type");
}

await db.update(pageBlocksTable).set({ data: patchedData.data });

Unknown slugs return { success: false }. Schema validation errors still throw from Zod so callers can use normal validation/error handling.

On this page