Cross-language metadata standard · Apache 2.0 · Installable in 5 languages

Make schema drift a compile-time error.

AI coding agents regenerate thousands of lines a day. Without a shared spine they produce inconsistent boilerplate at every prompt — and the drift surfaces as a production incident, not a compile error. MetaObjects is the spine: define typed entities once, generate idiomatic code across TypeScript, Java, Kotlin, C#.NET, and Python, and every generated stack breaks the type checker the moment the metadata moves underneath it.

meta gen — one metadata model, a full typed stack generated.

The problem MetaObjects solves, what it actually is, and how the codegen and runtime halves work together — in eight minutes.

One model — your objects, database, API, UI, and AI.

Codegen

One metadata model → idiomatic native code in five languages: Drizzle + Zod + Fastify/Hono (TS), Spring REST + DTO + repositories (Java), Exposed + KotlinPoet + Spring (Kotlin), EF Core + ASP.NET (C#), Pydantic + FastAPI (Python). Hand-edit-preserving regen via three-way merge.

Runtime metadata

Load metadata at runtime and drive behavior dynamically — CRUD, validation, relationships, dynamic admin UIs. (Typed tool payloads are declared today; MCP tool exposure is on the roadmap.) Less code in the repo; less surface area for AI agents to drift on.

Drift detection

meta verify validates every generated artifact against the metadata at build time. Renaming a field in one place breaks the type checker everywhere it's used — across services, across languages, across the human-and-AI-generated boundary.

Prompt construction

The prompt is code too — not a string scattered across services. Declare a typed payload as a projection, keep the text external and provider-resolved, render deterministically: snapshot-testable, cache-stable (no stray whitespace silently breaking exact-prefix prompt-cache hits), drift-checked at build time so a renamed field can't quietly degrade a prompt. Plus typed template.output parsers — Zod / Pydantic / Jackson / kotlinx.serialization / System.Text.Json across the ports.

Read: Prompts are code now — even OpenAI says so →

Requirements and testing

A capability is a node in the same model as your entities, so implementedBy is resolved, not trusted — a claim whose implementation vanished fails the build. meta verify reports the ledger on every run. The vocabulary and its checks ship in all five languages; requirementTests() scaffolds a test per claim in TypeScript.

Read: capability requirements →

AI coding agents regenerate thousands of lines a day. A field rename in one file misses three others — types compile, runtime breaks at the boundary, hours to chase. A required field added to an entity is forgotten in the migration, the validator, the form. A prompt template references {{userName}} while the payload type carries user.name. Without a shared spine, every prompt accelerates the entropy. MetaObjects inverts the source of truth: generated code is owned by the codegen, not by humans or agents.

Drift mode Without MetaObjects With MetaObjects
Rename created_atcreatedAt in the schema; miss the validator + form binding + API doc + tests Types compile. Runtime breaks at the boundary. Hours to chase. Edit one YAML field, regen. Six files update consistently. Drift breaks the build.
Add a new entity (table + validator + types + queries + routes + barrel = 6 files) AI misses one. App half-works. One YAML drop + meta gen.
Prompt template references a field the payload type doesn't carry Silent runtime failure in production. meta verify catches at build time. CI fails before merge.
Required field added to entity; forgotten in migration / validator / form Production NULL error. CHECK constraint + Zod refinement + form validation regenerate from one declaration.
Enum transition hand-coded as string literals across 5 files; agent invents "completed" instead of "complete" Silent string mismatch. Workflow stalls. field.enum @values narrows the literal type. Compile error on first wrong character.
TS service renames a column; the Java consumer's POJO mapping silently goes stale Boundary drifts. Cross-service schema mismatch in production. Same metadata regenerates both stacks. Conformance corpus guarantees they agree byte-for-byte.

The win compounds with scale. For a 5k-line app, MetaObjects is overkill. For a 50k-line codebase maintained by humans-plus-agents, drift is the dominant cost of change — and MetaObjects pays back the setup cost within the first month.

Drift is your code disagreeing with your model, and meta verify already makes that a build error. There is a second failure it cannot see: your code agrees with your model perfectly, and the model describes a capability your software does not have. The columns exist, the types line up, the tests pass — and no code path ever writes the value. Nothing is broken, so nothing fails.

Declared across three shipping products, that check found a bot that could not be retired, a season that could not be run privately, private tables silently running at the database default, and an interrupted-work recovery path that was never built — in a codebase with 1,280 tests passing and zero failures. A capability declared in MetaObjects is a node in the same model as your entities, so its link to the field that implements it is resolved rather than trusted.

How capability requirements work →

MetaObjects ships a hosted fit & migration assessment: one Markdown prompt your coding agent runs against your existing repo. Read-only, propose-only — it installs nothing, edits nothing, needs no database connection and no signup. Your agent reads the code, the migrations, and the commit log, then writes a decision-grade report whose centerpiece is a drift ledger built from your own history: the shapes you declare twice, where the copies disagree today, the past fixes that patched one copy and missed the other — and, per finding, the verify gate that would have made it a build failure instead of an incident.

Fetch https://metaobjects.dev/assess.md and run the MetaObjects
Fit & Migration Assessment against this repository.

Works in Claude Code, Cursor, Windsurf, GitHub Copilot, Gemini CLI — any agent that can fetch a URL. No agent open right now? Read the assessment prompt — it's one Markdown file; pasting it into any chat works too. The catch, stated plainly: it runs in your agent on your tokens, findings vary by model, and every claim is cited to a file:line or a commit precisely so you can check it. Nothing is sent to us; the report stays in your repo.

It's built to say no. Per-pillar verdicts include NOT A FIT, every promise is capped to what your language's port actually ships, and a "what you will NOT get" section is mandatory. In a blinded retro-test on a real pre-adoption production codebase, the assessment found specific, git-verified drift incidents that had already bitten — a constraint mismatch repaired only after a production violation, a schema divergence still live at assessment time — and its errors ran conservative, not inflated.

Language Status Details
TypeScript npm 0.24.5 · reference npm @metaobjectsdev/*@0.24.5 (the reference implementation). Drizzle + Zod + Fastify + Hono codegen, Kysely runtime, and meta migrate for Postgres / SQLite / Cloudflare D1 — the TS toolchain owns schema migrations. 2,500+ tests across the workspace.
Java Maven Central 7.24.5 com.metaobjects:metaobjects-*:7.24.5 on Maven Central. Spring REST + DTO + Repository + filter-allowlist codegen via codegen-spring; OMDB runtime persistence (CRUD / query / typed jsonb / Spring-tx); render + payload + verify + output-parser codegen.
Kotlin Maven Central 7.24.5 com.metaobjects:metaobjects-codegen-kotlin:7.24.5 + metadata-ktx Kotlin facade. KotlinPoet codegen: Exposed Tables, Spring controllers, payload VOs, output parsers, stored-proc helpers. Persistence-conformance against Testcontainers Postgres.
C#.NET NuGet 0.24.5 NuGet MetaObjects / MetaObjects.Render / MetaObjects.Codegen + the dotnet meta tool (MetaObjects.Cli), all 0.24.5. EF Core entities + AppDbContext + ASP.NET minimal-API routes codegen; render engine + payload-VO codegen + verify. (Schema migrations are TS-owned.)
Python PyPI 0.24.5 PyPI metaobjects 0.24.5 + the metaobjects CLI. Loader + canonical serializer + render + verify + codegen (Pydantic + FastAPI) + ObjectManager runtime. All five cross-port conformance corpora green.

Nineteen shared conformance corpora at fixtures/ — metamodel (253 fixtures), render, verify, extract, persistence (24 query scenarios against Testcontainers Postgres), API contract (cross-port, reference and generated lanes), YAML and more — validate every implementation against the same expectations. All ports implement metamodelVersion 0.13 — the metadata contract, versioned separately from the package lines.

A typed entity in metadata, side-by-side with what every port actually generates. Each output is idiomatic for that language — Drizzle + Zod for TypeScript, Spring REST + DTO records for Java, Exposed + KotlinPoet data classes for Kotlin, EF Core + ASP.NET classes for C#, Pydantic + FastAPI for Python. Same metadata; five idiomatic outputs; conformance-gated to byte-identical canonical form.

- object.entity:
    name: Subscriber
    children:
      - source.rdb:      { table: subscribers }
      - field.long:      { name: id }
      - field.string:    { name: email, maxLength: 320, required: true }
      - field.string:    { name: name }
      - field.enum:
          name: status
          values: [active, paused, cancelled]
          required: true
      - field.timestamp: { name: createdAt, column: created_at, autoSet: onCreate }
      - identity.primary: { name: primary, fields: [id], generation: increment }
// @generated by @metaobjectsdev/codegen-ts — DO NOT EDIT.export const subscribers = sqliteTable(
  "subscribers",
  {
    id: integer("id").primaryKey({ autoIncrement: true }),
    email: text("email").notNull(),
    name: text("name"),
    status: text("status", {
      enum: ["active", "paused", "cancelled"] as const,
    }).notNull(),
    createdAt: text("created_at").$defaultFn(() => new Date().toISOString()),
  },
…
);
export type Subscriber = InferSelectModel<typeof subscribers>;
export type SubscriberInsert = InferInsertModel<typeof subscribers>;
…
export const SubscriberInsertSchema = z.object({
  email: z.string().min(1).max(320),
  name: z.string().optional(),
  status: z.enum(["active", "paused", "cancelled"]),
…
});
…
Show the whole generated file (116 lines)
// @generated by @metaobjectsdev/codegen-ts — DO NOT EDIT.
// Source metadata: Subscriber (Subscriber)
// Customize via Subscriber.extra.ts in this directory.
import { type InferInsertModel, type InferSelectModel, sql } from "drizzle-orm";
import { check, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { z } from "zod";

export const subscribers = sqliteTable(
  "subscribers",
  {
    id: integer("id").primaryKey({ autoIncrement: true }),
    email: text("email").notNull(),
    name: text("name"),
    status: text("status", {
      enum: ["active", "paused", "cancelled"] as const,
    }).notNull(),
    createdAt: text("created_at").$defaultFn(() => new Date().toISOString()),
  },
  (table) => [
    check(
      "subscribers_status_chk",
      sql`status IN ('active', 'paused', 'cancelled')`,
    ),
  ],
);
export type Subscriber = InferSelectModel<typeof subscribers>;
export type SubscriberInsert = InferInsertModel<typeof subscribers>;
export type SubscriberUpdate = Partial<SubscriberInsert>;
export type SubscriberStatus = "active" | "paused" | "cancelled";
export const SubscriberInsertSchema = z.object({
  email: z.string().min(1).max(320),
  name: z.string().optional(),
  status: z.enum(["active", "paused", "cancelled"]),
  createdAt: z
    .string()
    .optional()
    .transform(() => new Date().toISOString()),
});

export const SubscriberUpdateSchema = z.object({
  email: z.string().min(1).max(320).optional(),
  name: z.string().optional().nullable(),
  status: z.enum(["active", "paused", "cancelled"]).optional(),
});

/** Typed patch shape for Subscriber: every settable field, optional (FR-035 PATCH). A
 * renamed/dropped field is a compile error at every `updateSubscriber` call site. */
export type SubscriberPatch = z.input<typeof SubscriberUpdateSchema>;

/** Insert-shape for import / restore / replication of Subscriber: identical to
 * SubscriberInsertSchema, but the @autoSet timestamp columns are written VERBATIM
 * (no create-time now() stamp) so the caller's original values are preserved. */
export const SubscriberInsertPreservingSchema = z.object({
  email: z.string().min(1).max(320),
  name: z.string().optional(),
  status: z.enum(["active", "paused", "cancelled"]),
  createdAt: z.string().optional(),
});
/**
 * Metadata constants for Subscriber.
 *
 * Use these instead of magic strings so TS catches typos and refactors stay
 * coherent. Each non-dollar-prefixed key is a per-field object carrying
 * name, label, view, optional htmlType/placeholder/helpText, and the
 * RHF-shaped validation rules derived from the field's validator children.
 *
 * Typical usage with the metaobjects React form helper:
 *
 *   import { useEntityForm } from '@metaobjectsdev/react';
 *   const form = useEntityForm(Subscriber, SubscriberInsertSchema);
 *   <input {...form.input.id} />
 */
export const Subscriber = {
  $entity: "Subscriber",
  $table: "subscribers",
  $path: "/subscribers",
  $apiPrefix: "/api",
  id: { name: "id", label: "Id", view: "number", htmlType: "number" },
  email: {
    name: "email",
    label: "Email",
    view: "text",
    htmlType: "text",
    rules: {
      required: "Email is required",
      maxLength: { value: 320, message: "Must be 320 characters or fewer" },
    },
  },
  name: { name: "name", label: "Name", view: "text", htmlType: "text" },
  status: {
    name: "status",
    label: "Status",
    view: "text",
    htmlType: "text",
    rules: { required: "Status is required" },
  },
  createdAt: {
    name: "createdAt",
    label: "Created At",
    view: "date",
    htmlType: "date",
  },
} as const;
import type { FilterAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";

export const SubscriberFilterAllowlist = {} as const satisfies FilterAllowlist;
import type { SortAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";

export const SubscriberSortAllowlist = {} as const satisfies SortAllowlist;
export type SubscriberFilter = {
  limit?: number;
  offset?: number;
  sort?: string;
  or?: SubscriberFilter[];
  and?: SubscriberFilter[];
};
/** GENERATED — wire DTO for Subscriber. Do not hand-edit; regenerated from metadata. */
public record SubscriberDto(
    Long id,
    @NotNull @Size(min = 1, max = 320) String email,
    String name,
    @NotNull SubscriberStatus status,
    java.time.Instant createdAt
) {
    public enum SubscriberStatus { active, paused, cancelled }

    /** Issue #203 @autoSet: a copy with every @autoSet column (onCreate AND onUpdate) stamped to now() on insert (the model value is ignored; a fresh row's updated_at equals its created_at). */
    public static SubscriberDto stampForInsert(SubscriberDto dto) {
        java.time.Instant __nowInstant = java.time.Instant.now();
        return new SubscriberDto(
            dto.id(),
            dto.email(),
            dto.name(),
            dto.status(),
            __nowInstant
        );
    }
…
}
…
Show the whole generated file (40 lines)
package acme;

import jakarta.validation.constraints.*;

/** GENERATED — wire DTO for Subscriber. Do not hand-edit; regenerated from metadata. */
public record SubscriberDto(
    Long id,
    @NotNull @Size(min = 1, max = 320) String email,
    String name,
    @NotNull SubscriberStatus status,
    java.time.Instant createdAt
) {
    public enum SubscriberStatus { active, paused, cancelled }

    /** Issue #203 @autoSet: a copy with every @autoSet column (onCreate AND onUpdate) stamped to now() on insert (the model value is ignored; a fresh row's updated_at equals its created_at). */
    public static SubscriberDto stampForInsert(SubscriberDto dto) {
        java.time.Instant __nowInstant = java.time.Instant.now();
        return new SubscriberDto(
            dto.id(),
            dto.email(),
            dto.name(),
            dto.status(),
            __nowInstant
        );
    }

    /** Issue #203 @autoSet: a copy with @autoSet onUpdate columns stamped to now(); onCreate columns are preserved (never rewrites created_at — the full-DTO update path). */
    public static SubscriberDto stampForUpdate(SubscriberDto dto) {
        return new SubscriberDto(
            dto.id(),
            dto.email(),
            dto.name(),
            dto.status(),
            dto.createdAt()
        );
    }

    /** Issue #203 @autoSet escape hatch: the DTO written VERBATIM (import/restore/replication). */
    public static SubscriberDto insertPreserving(SubscriberDto dto) { return dto; }
}
/**
 * GENERATED — do not hand-edit. Regenerated from metadata.
 */
public data class Subscriber(
  public val id: Long? = null,
  @field:NotNull
  @field:Size(min = 1, max = 320)
  public val email: String,
  public val name: String? = null,
  @field:NotNull
  public val status: SubscriberStatus,
  public val createdAt: Instant? = null,
)
Show the whole generated file (21 lines)
package acme

import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Size
import java.time.Instant
import kotlin.Long
import kotlin.String

/**
 * GENERATED — do not hand-edit. Regenerated from metadata.
 */
public data class Subscriber(
  public val id: Long? = null,
  @field:NotNull
  @field:Size(min = 1, max = 320)
  public val email: String,
  public val name: String? = null,
  @field:NotNull
  public val status: SubscriberStatus,
  public val createdAt: Instant? = null,
)
// <auto-generated/>
…
[Table("subscribers")]
public class Subscriber
{
    public enum SubscriberStatus { active, paused, cancelled }
    [Key]
    [Column("id")]
    public long Id { get; set; }
    [Column("email")]
    [Required(AllowEmptyStrings = true)]
    [MaxLength(320)]
…
    public string Email { get; set; } = default!;
    [Column("name")]
    public string? Name { get; set; }
    [Column("status")]
    public SubscriberStatus Status { get; set; }
    [Column("created_at")]
    public DateTimeOffset? CreatedAt { get; set; }
}
Show the whole generated file (29 lines)
// <auto-generated/>
// Generated by MetaObjects entity-generator. Do not edit by hand.
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Generated;

[Table("subscribers")]
public class Subscriber
{
    public enum SubscriberStatus { active, paused, cancelled }
    [Key]
    [Column("id")]
    public long Id { get; set; }
    [Column("email")]
    [Required(AllowEmptyStrings = true)]
    [MaxLength(320)]
    [MinLength(1)]
    public string Email { get; set; } = default!;
    [Column("name")]
    public string? Name { get; set; }
    [Column("status")]
    public SubscriberStatus Status { get; set; }
    [Column("created_at")]
    public DateTimeOffset? CreatedAt { get; set; }
}
# @generated by metaobjects — DO NOT EDIT.import datetime
from typing import Literal

from pydantic import BaseModel, Field


class Subscriber(BaseModel):
    id: int | None = None
    email: str = Field(max_length=320)
    name: str | None = None
    status: Literal["active", "paused", "cancelled"]
    createdAt: datetime.datetime | None = None
Show the whole generated file (36 lines)
# @generated by metaobjects — DO NOT EDIT.
# Source metadata: Subscriber (acme::Subscriber)
# Customize via Subscriber_extra.py in this directory.

from __future__ import annotations

import datetime
from typing import Literal

from pydantic import BaseModel, Field


class Subscriber(BaseModel):
    id: int | None = None
    email: str = Field(max_length=320)
    name: str | None = None
    status: Literal["active", "paused", "cancelled"]
    createdAt: datetime.datetime | None = None


class SubscriberCreate(BaseModel):
    """GENERATED — CREATE input: auto-gen PK / @mutability readOnly omitted (writeOnce is settable here, once); @default/@autoSet optional; present values validated (FR-036)."""

    email: str = Field(min_length=1, max_length=320)
    name: str | None = None
    status: Literal["active", "paused", "cancelled"]
    createdAt: datetime.datetime | None = None


class SubscriberPatch(BaseModel):
    """GENERATED — PATCH input: all fields optional (PK, @mutability readOnly and writeOnce excluded); present values validated (FR-036)."""

    email: str | None = Field(default=None, min_length=1, max_length=320)
    name: str | None = None
    status: Literal["active", "paused", "cancelled"] | None = None
    createdAt: datetime.datetime | None = None
CREATE TABLE "subscribers" (
  "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  "email" VARCHAR(320) NOT NULL,
  "name" TEXT,
  "status" TEXT NOT NULL,
  "created_at" TEXT DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT "subscribers_status_chk" CHECK ("status" IN ('active', 'paused', 'cancelled'))
);
# Scaffold metaobjects/ + .metaobjects/ + metaobjects.config.ts
$ meta init

# Generate code (entities, validators, queries, routes, payload VOs, output parsers)
$ meta gen

# Introspect the live DB, diff it against the metadata, write + apply the SQL
$ meta migrate --from-db --db file:dev.sqlite --dialect sqlite --slug init --apply

Conformance fixtures (JSON form): fixtures/conformance/. YAML and JSON are equivalent — pick whichever your stack prefers.

Fields aren't just columns. They carry validators, views, and currency formatting as child metadata. Entities derive read-only projections — passthrough fields and aggregates over relationships — that generate as database views, not tables. Everything below is real meta gen output, conformance-gated, not hand-wired annotations.

# -- Author — minimal identity entity; exists so ProgramSummary has a real
#    cross-entity edge to demonstrate `origin.passthrough` over a join. --
- object.entity:
    name: Author
    children:
      - source.rdb: { table: authors }
      - field.uuid: { name: id }
      - field.string: { name: name, required: true, maxLength: 120 }
      # A field carries VALIDATORS as child metadata, not just a column type.
      # These generate Zod refinements in TS and the equivalent constraint
      # annotations in every other port — nothing is hand-wired.
      - field.string:
          name: email
          required: true
          maxLength: 320
          children:
            # UNANCHORED on purpose: validator.regex @pattern is FULL-MATCH
            # (FR-036), so codegen wraps this in ^(?:…)$ itself. Adding
            # anchors here emits ^(?:^…$)$.
            - validator.regex: { name: emailFmt, pattern: "[^@]+@[^@]+" }
      - field.string:
          name: bio
          maxLength: 2000
          children:
            - validator.length: { name: bioLen, min: 0, max: 500 }
      - identity.primary: { name: id, fields: id, generation: uuid }
export const AuthorInsertSchema = z.object({
  name: z.string().min(1).max(120),
  email: z.string().min(1).max(320).regex(new RegExp("^(?:[^@]+@[^@]+)$")),
  bio: z.string().max(500).optional(),
});
…
Show the whole generated file (101 lines)
// @generated by @metaobjectsdev/codegen-ts — DO NOT EDIT.
// Source metadata: Author (Author)
// Customize via Author.extra.ts in this directory.
import type { InferInsertModel, InferSelectModel } from "drizzle-orm";
import { pgTable, uuid, varchar } from "drizzle-orm/pg-core";
import { z } from "zod";

export const authors = pgTable("authors", {
  id: uuid("id").primaryKey().defaultRandom(),
  name: varchar("name", { length: 120 }).notNull(),
  email: varchar("email", { length: 320 }).notNull(),
  bio: varchar("bio", { length: 2000 }),
});
export type Author = InferSelectModel<typeof authors>;
export type AuthorInsert = InferInsertModel<typeof authors>;
export type AuthorUpdate = Partial<AuthorInsert>;
export const AuthorInsertSchema = z.object({
  name: z.string().min(1).max(120),
  email: z.string().min(1).max(320).regex(new RegExp("^(?:[^@]+@[^@]+)$")),
  bio: z.string().max(500).optional(),
});

export const AuthorUpdateSchema = z.object({
  name: z.string().min(1).max(120).optional(),
  email: z
    .string()
    .min(1)
    .max(320)
    .regex(new RegExp("^(?:[^@]+@[^@]+)$"))
    .optional(),
  bio: z.string().max(500).optional().nullable(),
});

/** Typed patch shape for Author: every settable field, optional (FR-035 PATCH). A
 * renamed/dropped field is a compile error at every `updateAuthor` call site. */
export type AuthorPatch = z.input<typeof AuthorUpdateSchema>;
/**
 * Metadata constants for Author.
 *
 * Use these instead of magic strings so TS catches typos and refactors stay
 * coherent. Each non-dollar-prefixed key is a per-field object carrying
 * name, label, view, optional htmlType/placeholder/helpText, and the
 * RHF-shaped validation rules derived from the field's validator children.
 *
 * Typical usage with the metaobjects React form helper:
 *
 *   import { useEntityForm } from '@metaobjectsdev/react';
 *   const form = useEntityForm(Author, AuthorInsertSchema);
 *   <input {...form.input.id} />
 */
export const Author = {
  $entity: "Author",
  $table: "authors",
  $path: "/authors",
  $apiPrefix: "/api",
  id: { name: "id", label: "Id", view: "text", htmlType: "text" },
  name: {
    name: "name",
    label: "Name",
    view: "text",
    htmlType: "text",
    rules: {
      required: "Name is required",
      maxLength: { value: 120, message: "Must be 120 characters or fewer" },
    },
  },
  email: {
    name: "email",
    label: "Email",
    view: "text",
    htmlType: "text",
    rules: {
      pattern: { value: /[^@]+@[^@]+/, message: "Invalid format" },
      required: "Email is required",
      maxLength: { value: 320, message: "Must be 320 characters or fewer" },
    },
  },
  bio: {
    name: "bio",
    label: "Bio",
    view: "text",
    htmlType: "text",
    rules: {
      minLength: { value: 0, message: "Must be at least 0 characters" },
      maxLength: { value: 500, message: "Must be 500 characters or fewer" },
    },
  },
} as const;
import type { FilterAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";

export const AuthorFilterAllowlist = {} as const satisfies FilterAllowlist;
import type { SortAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";

export const AuthorSortAllowlist = {} as const satisfies SortAllowlist;
export type AuthorFilter = {
  limit?: number;
  offset?: number;
  sort?: string;
  or?: AuthorFilter[];
  and?: AuthorFilter[];
};
priceCents: {
    name: "priceCents",
    label: "Price Cents",
    view: "currency",
    currency: "USD",
    locale: "en-US",
  },
…
Show the whole generated file (165 lines)
// @generated by @metaobjectsdev/codegen-ts — DO NOT EDIT.
// Source metadata: Program (Program)
// Customize via Program.extra.ts in this directory.
import { type InferInsertModel, type InferSelectModel, sql } from "drizzle-orm";
import {
  type AnyPgColumn,
  bigint,
  check,
  jsonb,
  pgTable,
  text,
  uuid,
  varchar,
} from "drizzle-orm/pg-core";
import { z } from "zod";
import { authors } from "./Author";
import {
  InstructorProfile,
  InstructorProfileInsertSchema,
} from "./InstructorProfile";
import {
  SyllabusSection,
  SyllabusSectionInsertSchema,
} from "./SyllabusSection";

export const programs = pgTable(
  "programs",
  {
    id: uuid("id").primaryKey().defaultRandom(),
    title: varchar("title", { length: 200 }).notNull(),
    status: text("status", {
      enum: ["draft", "published", "archived"] as const,
    }),
    summary: varchar("summary", { length: 2000 }),
    priceCents: bigint("price_cents", { mode: "number" }),
    coverKey: varchar("cover_key", { length: 80 }),
    authorId: uuid("author_id")
      .notNull()
      .references((): AnyPgColumn => authors.id),
    syllabus: jsonb("syllabus").$type<SyllabusSection[]>(),
    instructorProfile: jsonb("instructor_profile").$type<InstructorProfile>(),
  },
  (table) => [
    check(
      "programs_status_chk",
      sql`status IN ('draft', 'published', 'archived')`,
    ),
  ],
);
export type Program = InferSelectModel<typeof programs>;
export type ProgramInsert = InferInsertModel<typeof programs>;
export type ProgramUpdate = Partial<ProgramInsert>;
export type ProgramStatus = "draft" | "published" | "archived";
export const ProgramInsertSchema = z.object({
  title: z.string().min(1).max(200),
  status: z.enum(["draft", "published", "archived"]).optional(),
  summary: z.string().max(2000).optional(),
  priceCents: z.number().int().optional(),
  coverKey: z.string().max(80).optional(),
  authorId: z.string(),
  syllabus: z.array(SyllabusSectionInsertSchema).optional(),
  instructorProfile: InstructorProfileInsertSchema.optional(),
});

export const ProgramUpdateSchema = z.object({
  title: z.string().min(1).max(200).optional(),
  status: z.enum(["draft", "published", "archived"]).optional().nullable(),
  summary: z.string().max(2000).optional().nullable(),
  priceCents: z.number().int().optional().nullable(),
  coverKey: z.string().max(80).optional().nullable(),
  authorId: z.string().optional(),
  syllabus: z.array(SyllabusSectionInsertSchema).optional().nullable(),
  instructorProfile: InstructorProfileInsertSchema.optional().nullable(),
});

/** Typed patch shape for Program: every settable field, optional (FR-035 PATCH). A
 * renamed/dropped field is a compile error at every `updateProgram` call site. */
export type ProgramPatch = z.input<typeof ProgramUpdateSchema>;
/**
 * Metadata constants for Program.
 *
 * Use these instead of magic strings so TS catches typos and refactors stay
 * coherent. Each non-dollar-prefixed key is a per-field object carrying
 * name, label, view, optional htmlType/placeholder/helpText, and the
 * RHF-shaped validation rules derived from the field's validator children.
 *
 * Typical usage with the metaobjects React form helper:
 *
 *   import { useEntityForm } from '@metaobjectsdev/react';
 *   const form = useEntityForm(Program, ProgramInsertSchema);
 *   <input {...form.input.id} />
 */
export const Program = {
  $entity: "Program",
  $table: "programs",
  $path: "/programs",
  $apiPrefix: "/api",
  id: { name: "id", label: "Id", view: "text", htmlType: "text" },
  title: {
    name: "title",
    label: "Title",
    view: "text",
    htmlType: "text",
    rules: {
      required: "Title is required",
      maxLength: { value: 200, message: "Must be 200 characters or fewer" },
    },
  },
  status: { name: "status", label: "Status", view: "text", htmlType: "text" },
  summary: {
    name: "summary",
    label: "Summary",
    view: "textarea",
    rules: {
      maxLength: { value: 2000, message: "Must be 2000 characters or fewer" },
    },
  },
  priceCents: {
    name: "priceCents",
    label: "Price Cents",
    view: "currency",
    currency: "USD",
    locale: "en-US",
  },
  coverKey: {
    name: "coverKey",
    label: "Cover Key",
    view: "image",
    rules: {
      maxLength: { value: 80, message: "Must be 80 characters or fewer" },
    },
  },
  authorId: {
    name: "authorId",
    label: "Author Id",
    view: "text",
    htmlType: "text",
    rules: { required: "Author Id is required" },
  },
  syllabus: {
    name: "syllabus",
    label: "Syllabus",
    view: "text",
    htmlType: "text",
  },
  instructorProfile: {
    name: "instructorProfile",
    label: "Instructor Profile",
    view: "text",
    htmlType: "text",
  },
} as const;
import type { FilterAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";

export const ProgramFilterAllowlist = {} as const satisfies FilterAllowlist;
import type { SortAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";

export const ProgramSortAllowlist = {} as const satisfies SortAllowlist;
export type ProgramFilter = {
  limit?: number;
  offset?: number;
  sort?: string;
  or?: ProgramFilter[];
  and?: ProgramFilter[];
};
// @generated by @metaobjectsdev/codegen-ts — DO NOT EDIT.// View declaration — Drizzle uses this for typed SELECT queries.
// The SQL view is created/managed by migrate-ts; .existing() tells Drizzle
// not to attempt DDL for this declaration.
export const programSummaryView = pgView("v_program_summary", {
  id: uuid("id").notNull(),
  title: varchar("title", { length: 200 }).notNull(),
  authorName: text("author_name"),
  lessonCount: bigint("lesson_count", { mode: "number" }),
  completedRevenueCents: bigint("completed_revenue_cents", { mode: "number" }),
  hasCoverArt: boolean("has_cover_art"),
}).existing();
export const ProgramSummarySchema = z.object({
  id: z.string(),
  title: z.string(),
  authorName: z.string().nullable(),
  lessonCount: z.number().int().nullable(),
  completedRevenueCents: z.number().int().nullable(),
  hasCoverArt: z.boolean().nullable(),
});
…
Show the whole generated file (80 lines)
// @generated by @metaobjectsdev/codegen-ts — DO NOT EDIT.
// Source metadata: ProgramSummary (ProgramSummary)
import {
  bigint,
  boolean,
  pgView,
  text,
  uuid,
  varchar,
} from "drizzle-orm/pg-core";
import { z } from "zod";

// View declaration — Drizzle uses this for typed SELECT queries.
// The SQL view is created/managed by migrate-ts; .existing() tells Drizzle
// not to attempt DDL for this declaration.
export const programSummaryView = pgView("v_program_summary", {
  id: uuid("id").notNull(),
  title: varchar("title", { length: 200 }).notNull(),
  authorName: text("author_name"),
  lessonCount: bigint("lesson_count", { mode: "number" }),
  completedRevenueCents: bigint("completed_revenue_cents", { mode: "number" }),
  hasCoverArt: boolean("has_cover_art"),
}).existing();
export const ProgramSummarySchema = z.object({
  id: z.string(),
  title: z.string(),
  authorName: z.string().nullable(),
  lessonCount: z.number().int().nullable(),
  completedRevenueCents: z.number().int().nullable(),
  hasCoverArt: z.boolean().nullable(),
});
export type ProgramSummary = z.infer<typeof ProgramSummarySchema>;
export const ProgramSummary = {
  $entity: "ProgramSummary",
  $view: "v_program_summary",
  $path: "/program-summaries",
  $apiPrefix: "/api",
  id: { name: "id", label: "Id", view: "text", dbCol: "id" },
  title: { name: "title", label: "Title", view: "text", dbCol: "title" },
  authorName: {
    name: "authorName",
    label: "Author Name",
    view: "text",
    dbCol: "author_name",
  },
  lessonCount: {
    name: "lessonCount",
    label: "Lesson Count",
    view: "number",
    dbCol: "lesson_count",
  },
  completedRevenueCents: {
    name: "completedRevenueCents",
    label: "Completed Revenue Cents",
    view: "currency",
    dbCol: "completed_revenue_cents",
    currency: "USD",
    locale: "en-US",
  },
  hasCoverArt: {
    name: "hasCoverArt",
    label: "Has Cover Art",
    view: "checkbox",
    dbCol: "has_cover_art",
  },
} as const;
import type { FilterAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";

export const ProgramSummaryFilterAllowlist =
  {} as const satisfies FilterAllowlist;
import type { SortAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";

export const ProgramSummarySortAllowlist = {} as const satisfies SortAllowlist;
export type ProgramSummaryFilter = {
  limit?: number;
  offset?: number;
  sort?: string;
  or?: ProgramSummaryFilter[];
  and?: ProgramSummaryFilter[];
};

Run it yourself: every metadata and generated-code block on this page is cut from a committed example that loads and generates a full typed stack with zero errors and zero warnings, and the cuts are checked against the real generated files on every release — so a block here cannot drift from what the tool emits. Every construct it uses (projections, origins, currency views, field-level validators) is conformance-gated across all five ports.

For a single-language project that doesn't talk to LLMs at scale, the popular per-language ORMs are excellent and likely enough. MetaObjects adds value when (a) your codebase is touched by AI agents at volume, (b) you ship in more than one language, or (c) you have typed prompts and tool-calls that need governance. Honest comparison, by language:

Language Popular alternatives What they do well What MetaObjects adds
TypeScript Prisma, Drizzle, TypeORM, Kysely Mature schema → migration loops. Excellent type-safe SQL ergonomics (Drizzle, Kysely). Mature ecosystem. Same Drizzle + Zod output, plus four other languages from the same source. Plus typed prompt rendering, output parsers, tool-call envelopes, and meta verify drift gates.
Java Hibernate / JPA, jOOQ, Spring Data JPA Industry-standard ORM (Hibernate). Type-safe SQL with code-first generation (jOOQ). Repository abstractions (Spring Data). The same JPA-ready entities, plus the controller/DTO/repository layer, plus four other languages from the same source. Runtime reflection-free codegen is friendlier to GraalVM native-image and to AI grep.
Kotlin Exposed, Ktor + jOOQ, kotlinx.serialization Idiomatic Kotlin SQL DSL (Exposed). Solid web stack (Ktor). First-class coroutines. KotlinPoet-generated data classes + Exposed Tables from the same metamodel that drives the Java / TS / Python / C# stacks. Same Kotlin idioms; cross-port lockstep.
C#.NET EF Core, Dapper, NHibernate Microsoft-supported, deeply integrated with ASP.NET (EF Core). Lightweight micro-ORM speed (Dapper). EF Core entities + AppDbContext + ASP.NET routes from the metamodel. Plus the cross-port REST API contract — your C# server agrees with the TS / Java / Kotlin / Python services on filter operators, pagination envelopes, and 404 shapes.
Python SQLAlchemy, Django ORM, Pydantic + FastAPI, SQLModel Mature ORM (SQLAlchemy). Batteries-included framework (Django). Excellent validation (Pydantic). Pydantic schemas + FastAPI routers from the same metamodel that drives every other port. Same Pythonic outputs; same drift guarantees.
Schema-only IDLs OpenAPI, Smithy, gRPC / protobuf Strong API-shape governance. Wide client/server tooling. Industry-standard. Describes the whole stack, not just the API shape: persistence + render + tool-calls + UI bindings + validation, all from one declaration. IDLs give you type stubs; MetaObjects gives you the table, the validator, the route, the prompt template, and the migration.

Capability matrix — including AI-agent suitability.

Where it matters: the right-hand four columns are the AI-coding-agent dimensions. They were the design driver, not an afterthought.

Capability MetaObjects Prisma / Drizzle JPA / EF Core / SQLAlchemy OpenAPI / Smithy
Idiomatic per-language codegen 5 languages TS only single-lang type stubs only
Cross-language source of truth five ports, byte-identical no no API shape only
Schema migrations diff + emit + apply (TS) yes yes no
Typed validation at the wire boundary Zod / Pydantic / Jakarta / DataAnnotations / Jakarta runtime only annotations schema-only
Runtime metadata (drive behavior dynamically) TS / Java / Python no reflection no
Drift detection (build-time) meta verify migration diff migration diffno
Typed prompt construction + render 5 ports, conformance-gated no no no
template.output parser-on-receipt 5 ports (Zod / Pydantic / Jackson / kotlinx / STJ) no no no
LLM tool-call envelopes (template.toolcall) declared cross-port no no no
Hand-edit-preserving regen 3-way merge user-edits adjacent scaffolding only codegen reruns
Machine-readable metadata (grep-friendly) YAML/JSON, named constants DSL or schema.prisma annotations / decorators YAML/JSON
Generated code is plain (no proxy/decorator magic) idiomatic native idiomatic proxy/runtime magictype stubs
Deterministic regen (same in → same out) conformance-gatedyes yes yes
Small surface area for agent context window13 base types + extensions single-lang ORM API large framework surfacesmall spec

The honest read: for a 5k-line TS app with no LLM integration, Drizzle is probably enough. For a 50k-line codebase maintained by humans-plus-agents, drift is the dominant cost of change — and MetaObjects' build-time drift gate alone justifies the setup. For a multi-port team (TS + Java, or TS + Python, or all five), no other system in this matrix does cross-language metadata sharing at all.

Every contract is grep-friendly (named constants, never magic strings). Generated code is idiomatic per-language, not framework-magic. An llms.txt index ships at the root so any agent fluent in the convention can index the full standard in seconds. The substrate is designed so Claude Code, Cursor, GitHub Copilot, and Windsurf edit metadata files — and the codegen handles every language behind them.

Why Claude Code in particular. Anthropic's official CLI was the day-zero driver for the standard's design choices. The metamodel vocabulary is small enough to fit in a context window; the conformance contract is enforced as compile-time errors rather than runtime drift; and the generated code is the same idiomatic per-language output a senior engineer would write by hand, so an agent's review pass converges fast. A first-party MCP server exposing the spec, conformance fixtures, and codegen tools as agent-callable functions is on the roadmap.

Client-side

Universal across backends -- a Java server serving React uses the same TypeScript packages.

MetaObjects isn't new. Built in 2001 and run privately for 25 years — shipping secure messaging, payments, online gaming, and pharmaceutical-regulatory systems, with one approach patented in the US. Set aside in 2022; now open-sourced and rebuilt AI-first, for the first time. The same spine that kept those systems coherent is the one that now keeps AI-generated code from drifting.

Read the story →