Don't take the table's word for it. Ask your own git history.
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.
With your repo open in your agent, send one message — read-only · no install · no signup
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.
From one schema, five languages.
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.
The source — the Subscriber entity, from metaobjects/meta.subscriber.yaml in package acme
- 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 }
TypeScript — the Drizzle table and Zod schemas, from codegen-ts (same run: typed queries, Fastify/Hono routes, a barrel)
…
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)
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(),
});
export type SubscriberPatch = z.input<typeof SubscriberUpdateSchema>;
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(),
});
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[];
};
Java — the DTO record, from codegen-spring (same run: the @RestController, a repository interface, filter allowlists)
…
public record SubscriberDto(
Long id,
@NotNull @Size(min = 1, max = 320) String email,
String name,
SubscriberStatus status,
java.time.Instant createdAt
) {
public enum SubscriberStatus { active, paused, cancelled }
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.*;
public record SubscriberDto(
Long id,
@NotNull @Size(min = 1, max = 320) String email,
String name,
SubscriberStatus status,
java.time.Instant createdAt
) {
public enum SubscriberStatus { active, paused, cancelled }
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
);
}
public static SubscriberDto stampForUpdate(SubscriberDto dto) {
return new SubscriberDto(
dto.id(),
dto.email(),
dto.name(),
dto.status(),
dto.createdAt()
);
}
public static SubscriberDto insertPreserving(SubscriberDto dto) { return dto; }
}
Kotlin — the data class, from codegen-kotlin (same run: the Exposed table, a Spring controller)
…
public data class Subscriber(
public val id: Long? = null,
(min = 1, max = 320)
public val email: String,
public val name: String? = null,
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
public data class Subscriber(
public val id: Long? = null,
(min = 1, max = 320)
public val email: String,
public val name: String? = null,
public val status: SubscriberStatus,
public val createdAt: Instant? = null,
)
C#.NET — the EF Core entity, from MetaObjects.Codegen (same run: the AppDbContext, minimal-API CRUD routes)
…
[]
public class Subscriber
{
public enum SubscriberStatus { active, paused, cancelled }
[]
[]
public long Id { get; set; }
[]
[]
[]
…
public string Email { get; set; } = default!;
[]
public string? Name { get; set; }
[]
public SubscriberStatus Status { get; set; }
[]
public DateTimeOffset? CreatedAt { get; set; }
}
Show the whole generated file (29 lines)
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Generated;
[]
public class Subscriber
{
public enum SubscriberStatus { active, paused, cancelled }
[]
[]
public long Id { get; set; }
[]
[]
[]
[]
public string Email { get; set; } = default!;
[]
public string? Name { get; set; }
[]
public SubscriberStatus Status { get; set; }
[]
public DateTimeOffset? CreatedAt { get; set; }
}
Python — the Pydantic model, from metaobjects gen (same run: the FastAPI router, filter allowlists)
…
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)
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
Plus the migration meta migrate emits — dialect-aware (this one SQLite; Postgres and Cloudflare D1 too)
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'))
);
And the three CLI commands you actually run
$ meta init
$ meta gen
$ 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.
The metamodel goes deep.
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.
The source — Author, whose fields carry validators as child metadata (from examples/advanced-modeling, package acme::learn)
- object.entity:
name: Author
children:
- source.rdb: { table: authors }
- field.uuid: { name: id }
- field.string: { name: name, required: true, maxLength: 120 }
- field.string:
name: email
required: true
maxLength: 320
children:
- 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 }
Field validators → Zod refinements (generated/Author.ts)
…
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)
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(),
});
export type AuthorPatch = z.input<typeof AuthorUpdateSchema>;
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[];
};
Currency view → typed formatting metadata — Program.priceCents is a field.currency with a view.currency child (generated/Program.ts)
…
priceCents: {
name: "priceCents",
label: "Price Cents",
view: "currency",
currency: "USD",
locale: "en-US",
},
…
Show the whole generated file (165 lines)
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(),
});
export type ProgramPatch = z.input<typeof ProgramUpdateSchema>;
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[];
};
Projection → a read-only database view, not a table — ProgramSummary passes through Author.name and aggregates over its lessons and purchases (generated/ProgramSummary.ts)
…
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)
import {
bigint,
boolean,
pgView,
text,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { z } from "zod";
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.