MetaObjects is a cross-language metadata standard for declaring typed entity models that drive code generation, runtime metadata access, and drift detection — across TypeScript, C#, Java, Python, and Kotlin (Kotlin runs on the JVM via metadata-ktx + codegen-kotlin).
The metamodel is the durable spine; generated code is the disposable artifact. Substrate is local-first: typed metadata lives in your repo, generated code is idiomatic per-language output that runs without any MetaObjects dependency at runtime. If @metaobjectsdev/* disappears tomorrow, you keep working code.
Equal weight — all four ship per-language today across the five ports (TS / C# / Java / Python / Kotlin), with cross-port conformance corpora verifying byte-identical behavior:
- Codegen — emit idiomatic per-language code (Drizzle/Zod + Fastify for TS, EF Core + ASP.NET for C#, Spring REST + DTO + Repository for Java via
codegen-spring, Pydantic + FastAPI for Python, KotlinPoet + Exposed + Spring for Kotlin viacodegen-kotlin). Hand-edit-preserving regen via three-way merge. - Runtime metadata — load metadata at runtime, drive behavior dynamically (CRUD, validation, relationships, dynamic admin UIs, LLM tool registration). On Kysely (TS), a DB-API 2 driver via ObjectManager (Python), modernized JDBC + Spring-tx via OMDB (Java), Exposed (Kotlin), EF Core (C#).
- Drift detection —
meta verifycatches divergence between code and metadata (covers entity codegen, prompt templates, output parsers, schema). Quality-of-life on top of codegen + runtime. - Prompt construction — a prompt is code, not a string scattered across services. Declare a prompt's payload as a typed projection (payload bloat becomes a diff), keep its text external and provider-resolved, and render it deterministically: snapshot-testable, cache-stable (no whitespace change silently breaking exact-prefix prompt-cache hits), and drift-checked at build time so a renamed field can't degrade a prompt. Conformance-gated, so the guarantee holds in every language port. Render + payload-VO codegen +
verify+template.outputparser-on-receipt (FR-006) + the output-format prompt fragment & tolerantextractparser (FR-010) ship in all five ports today — the library-side building blocks of the pillar are complete. The one remaining library-side piece is MCP exposure of declared prompts/tools (seespec/roadmap.md); the application-level consolidation (eval harness, end-to-end declared-prompt orchestration) and consumer adoption are exercised in adopter projects, not in this library repo. Designed indocs/superpowers/specs/2026-05-22-fr-004-cross-language-prompt-construction-design.md.
Last refreshed 2026-07-20.
TypeScript reference implementation is published to npm at 0.19.1 (14 @metaobjectsdev/* publish candidates on the latest tag, full lockstep; the two angular packages are on their own 0.6.x line). C# at 0.19.1 (NuGet); Python at 0.19.2 (PyPI — the Python line runs one ahead after its own 0.19.1 hotfix); Java / Kotlin at 7.11.1 (Maven Central). The 0.19.1 line is a coordinated patch (no breaking changes, no new vocabulary): an explicitly authored validator.length @min is now authoritative over the FR-036 Pin 1 implicit non-empty floor — every port previously clamped an authored @min with max(@min, 1) and silently discarded it, so @min: 0 on a @required string was inexpressible; now @min: 0 restores "must be provided, may be empty" and any explicit @min always wins (#224); the @required vocabulary (spec files, embedded definitions, registry manifest, metaobjects-authoring skill) now matches that shipped behavior (#224 §A5); and meta verify --dialect d1 --d1 <binding> [--remote] can target Cloudflare D1 through the existing wrangler transport, with --db file: into .wrangler/state/**/d1/** now warning it is verifying the local database rather than silently doing so (#225). See CHANGELOG.md [0.19.1] (and the separate PyPI [0.19.1] entry for the Python-only hotfix folded into this line's 0.19.2). The prior 0.19.0 / 7.11.0 line was a coordinated additive minor (no breaking changes): image support — a metadata-driven view.image form control on field.string (storage is an opaque key; no image bytes cross the wire), whose generated <Entity>Form (codegen-ts-react) renders an upload/crop <ImageUpload> widget via a react-hook-form <Controller>, backed by a consumer-supplied ImageUploadAdapter (upload/imageUrl) over React context — plus a new metaobjects-ui-web TS-applied concern provider (spec/metamodel/ui-web.json) that now owns view.image's five presentation attrs (@aspectRatio, @maxEdge, @store, @accept, @maxBytes) and the previously-deferred @rows on view.textarea (core view.json still registers zero view attrs, so the FR-033 invariant holds; non-TS ports mirror the spec file but never apply it — TS-web-only). New client surface: @metaobjectsdev/runtime-web (canvasToJpegBlob/reencodeJpeg + adapter types) and @metaobjectsdev/react (<ImageUpload>, <ImageUploadAdapterProvider>/useImageUploadAdapter(), cropToBlob, an optional ./form.css export; react-easy-crop is an optional lazy-loaded peer). Also folds in a 0.18.0 test-only fixup (the @formExclude registration had left two full-suite tests red on main; regenerated, no re-release — published 0.18.0 product code was correct). See CHANGELOG.md [0.19.0]. The prior 0.18.0 / 7.10.0 line was a coordinated additive minor (no breaking changes): view-kind form-control dispatch in codegen-ts-react — the generated <Entity>Form now renders the right control per field view (enum→<select>, view.textarea→<textarea>, view.checkbox, view.radio; styled submit wrapper; aria-labels) instead of a bare <input> for every scalar — plus @formExclude registered cross-port on the field.* wildcard: the form template read it but core never registered it (strict verify rejected it as ERR_UNKNOWN_ATTR), now first-class in all five ports via the data-driven UI provider. Configurable @rows was deferred (no clean cross-port home for an attribute on a TS-only view subtype — ui.json extends throws where view.textarea is deregistered, core view.json breaks the FR-033 "core owns zero view attrs" invariant; textareas use a fixed rows={4}). See CHANGELOG.md [0.18.0]. The prior npm 0.17.1 was an npm-only patch on top of the 0.17.0 line (PyPI/NuGet/Maven unchanged): it fixes the write-through-entity generated REST routes to read through the replica view so read-your-writes returns the derived origin.passthrough field — the #214 read-half had shipped in the query layer + the view, but the routes layer was left mounting vanilla table CRUD (a readView on runtime-ts mountCrudRoutes + codegen-ts routes wiring), now gated by a cross-port api-contract-conformance write-through corpus (TS/C#/Kotlin). See CHANGELOG.md [0.17.1]. The 0.17.0 / 7.9.0 line is a coordinated additive minor bundling #195 projection read-model origins (origin.aggregate @agg: any|all|collect, origin.computed @expr, origin.first), #207 projection row-scope @filter (view outer WHERE), #208 DDL-ownership escape valves (source.rdb @sql hand-written body + @unmanaged marker; ADR-0043), #214 entity read-view codegen READ half (all five ports), and Program D value-object jsonb-column PATCH parity — plus a full documentation + agent-context skills refresh (the seven meta init skills accuracy-passed + Fable-reviewed; runtime-ui gained its Python + C# language refs). No breaking changes. See CHANGELOG.md [0.17.0]. The prior 0.16.0 / 7.8.0 line was a coordinated BREAKING release (FR-036) bundling the held FR-035 present-key PATCH tristate (absent≠null: absent→untouched / present-null→clears / present-null-on-@required→400) with FR-036 cross-port constraint-validation enforcement + semantic pins: @required string = NON-EMPTY (reject null/"", accept whitespace); validator.regex @pattern = FULL-MATCH; @maxLength×validator.length @max = strictest-wins; C# and Python now ENFORCE field constraints over HTTP (both were decorative at the wire tier); POST + PATCH validate present values on all five ports, vanilla AND TPH per-subtype. A missing @required value-type POST now 400s everywhere; a server-defaulted/@autoSet/auto-gen-PK @required field is correctly optional on POST. All conformance-gated (validation-conformance + api-contract, both lanes) — an xhigh review before release caught 11 cross-port divergences, all fixed + gated. See CHANGELOG.md [0.16.0]. The prior 0.15.21 / 7.7.11 line was a bug-fix release (npm 0.15.21 · PyPI 0.15.13 · Maven 7.7.11; NuGet unchanged at 0.15.10 — the C# port needed no fix and was the reference the other three were corrected against). No metadata changes, no new vocabulary. It closes a family of migrate-engine defects that were destructive or silent: a wrong-row write/delete in every writable mount (a numeric-looking string id on a TEXT/uuid PK hit a different row); every incremental meta migrate rebuilding every uuid-PK table; @autoSet emitting DEFAULT now() on SQLite/D1 (un-appliable migration); field.enum @values changes never migrating on SQLite (stale CHECK → production insert failures); the SQLite emitter dropping index @expr/@where/@orders (a partial-UNIQUE silently becoming fully UNIQUE); an auto-allowed drop-view; @kind: storedProc projections crashing meta migrate; and quoted boolean defaults stored as TEXT so = 0 matched nothing. It also fixes hardcoded primary-key types in the Java, Kotlin and Python generators — a @generation: uuid entity got broken output (Kotlin's did not compile) while its own DTO correctly used UUID. Every migrate fix is now gated by an emit → apply to a real engine → introspect → re-diff must be EMPTY round-trip plus value-semantics probes; the absence of that gate (nothing ever ran the pipeline twice against a real DB) is what let the class survive 5000+ tests. The prior 0.15.19 / 7.7.9 line was a coordinated release across all four registries (additive, non-breaking): origin.aggregate @filter (a scoping filter — an attr.filter object — rendered as SQL FILTER (WHERE …) / SQLite CASE WHEN) is now registered in all five ports + registry-conformance (with an origin-aggregate-filtered fixture), closing the "projections can't express my scoped aggregate" gap; and the agent-context skills + docs make the projection/DB-view contract explicit — a projection's CREATE VIEW is generated from its origin.* children, so a hand-written view is drift (and, being unmanaged, invisible to meta verify --db), with the "custom SQL views" hand-write exception bounded to genuinely-irreducible views, a new audit view-necessity signature + a VOCAB CANDIDATE new-subtype-opportunity hunt, and a new docs/features/downstream-metadata-decisions.md adopter guide. npm 0.15.18 was an npm-only, non-breaking patch on top of the coordinated 0.15.17 line (PyPI/NuGet/Maven unchanged — TS-only fixes; advances the 1.0 quiet period): the codegen-ts promptRender FQN @objectRef fix (a prompt payload value-object nesting an @objectRef to another value-object emitted invalid pkg::Name TypeScript — now package-stripped to the bare name, matching entityFile) plus a batch of agent-context skills fixes (four of six metaobjects-* skills had invalid YAML front-matter so they never intent-triggered; wrong C# reference flags; the reference-fragment install reverted from deploy-all to stack-scoped; deprecated codegen-ts/generators imports + singular tanstack paths; ADR-0040 index.lookup vocabulary; and a stale Kotlin fixture that had left agent-context-conformance red on main). A cross-port hunt confirmed the promptRender FQN leak was TS-only (Java/C#/Kotlin/Python payload codegen already strip); PR #190 added the missing Python + Kotlin regression tests. The 0.15.17 line is a coordinated release across all four registries: (1) BREAKING — origin.passthrough is type-preserving (a passthrough field must match its @from source's field.<subType> + array-ness or fail load with ERR_PASSTHROUGH_TYPE_MISMATCH; opt out with @convert: true; retires the narrow ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH) (#185/#186); (2) typed value-object jsonb columns end-to-end all ports — single + @isArray array-of-VO round-trip through every port's write/read codec; Kotlin codegen moved off kotlinx @Serializable to a generated per-package Jackson MetaJsonbMapper (no serialization compiler plugin), C#/Java/Python array-of-VO codecs fixed (#187); (3) load-order-independent super-resolution — a dotted extends: Owner.member to an inherited member now resolves regardless of file load order (was a Node-vs-Bun readdir build-portability bug) via on-demand memoized resolution (#188/#189). The 0.15.6 line is a coordinated cross-port loader-ordering fix (all four registries bumped together): an overlay-only file could be merged before the base file declaring the entity it re-opens, so an object.projection overlay re-open (and any overlay reaching a not-yet-loaded extends/origin target) failed super-resolution with spurious ERR_INVALID_ORIGIN / ERR_UNRESOLVED_SUPER / ERR_MISSING_REQUIRED_ATTR — surfacing as a TS-vs-Python divergence but latent in every port. Each loader now stable-partitions overlay-only sources/roots to merge last, deterministically, cross-port conformance-gated (projection-overlay-abstract-identity) (#160). The prior 0.15.5 / PyPI 0.15.6 line shipped two consumer-provider CLI fixes (offline meta migrate now threads metaobjects.config.ts providers #157; a Python metaobjects … --provider module:symbol hook #158). The 0.15.4 line fixes cross-package same-name root nodes being wrongly merged by the TS/C#/Python loaders (#155). 2500+ tests passing across the workspace. (The 0.15.1 / 7.7.1 line shipped the index.* type + identity.secondary key-purity [ADR-0040]: identity.secondary is now a unique alternate key with @unique removed — a legacy @unique fails load with ERR_UNKNOWN_ATTR — and a new index.lookup subtype carries non-unique retrieval [@fields required; @using/@expr/@where/@orders are RDB-physical escapes], cross-port conformance-gated; breaking, migration in docs/features/migrations/identity-secondary-to-index-lookup.md. This is on top of the 0.15.0 metamodel-1.0 vocabulary program + the ADR-0039 own-accessor fix.)
All five ports ship loader + canonical serializer + conformance + codegen + render + payload-VO + verify:
- TypeScript —
codegen-ts(Vite-style plugins; Drizzle, Zod, Fastify) +runtime-ts+migrate-ts+ the universal web client packages (runtime-web,react,tanstack). - C# —
MetaObjects(loader + canonical serializer + conformance) +MetaObjects.Render(Mustache + payload-VO +verify) +MetaObjects.Codegen(EF Core entities +AppDbContext+ CRUD minimal-API routes). Schema migrations are TS-owned (ADR-0015): the C# migrate engine and themigrate/--from-dbCLI surface were removed; the C# CLI isgen/verifyonly, packaged as a .NET tool invokeddotnet meta(not a baremeta— that name belongs to the Node schema CLI). EF Core runtime data-access stays per-port. - Java —
metadata+omdb+om+dynamic+core-spring+metadata-ktx(Kotlin facade) +codegen-spring(Spring controllers + DTOs + repositories + filter allowlists + payload records + output parsers) +codegen-mustache+codegen-plantuml+render+maven-plugin(meta:gen/meta:editor+ ameta:verifycodegen-drift goal — distinct from the removed live-DBmeta:verify; Kotlin generators run throughmeta:genvia the shared SPI). FR-003 (OMDB runtime persistence + binding registry + typed jsonb + Spring-tx + source/origin metamodel) fully shipped, including Plan 4 (engine-debt remediation: atomic mapping cache, JDBC codec registry,inTransactiontemplate). Schema migrations are owned by the TypeScript toolchain (@metaobjectsdev/cli migrate); the Java port's diff-and-converge migration engine and itsmeta:migrate/ live-DB-driftmeta:verifyMaven goals were removed, and per ADR-0015 Decision 2 the dev/test runtime auto-create path (MetaClassDBValidatorService+ the drivers'createTable/createIndex/createForeignKey/createSequenceDDL) is also removed — OMDB is pure data-access (CRUD/query/codec/transactions only). - Python —
metaobjects(loader + canonical serializer + conformance + render + verify + codegen) + anObjectManagerruntime layer. Themigratemodule was removed (schema is TS-owned, ADR-0015); Python is pure data-access (codegen + ObjectManager runtime), with ametaobjectsconsole-script (gen/verifycodegen — nomigrate). All five conformance corpora green. - Kotlin —
codegen-kotlin(KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators.integration-tests-kotlinruns the persistence-conformance corpus through Exposed against Testcontainers Postgres.
Cross-port conformance corpora (every port runs the shared corpus):
- Metamodel:
fixtures/conformance/(~90 fixtures). TS / C# / Java / Python all green. - Render:
fixtures/render-conformance/. TS / C# / Java / Kotlin / Python byte-identical. - Persistence:
fixtures/persistence-conformance/. Query scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-producedcanonical/schema.postgres.sql(Postgres only — Derby dropped for the cross-port query corpus, ADR-0015). The migration scenarios are exercised by TS only (TS owns schema migrations). The corpus now gates WRITES, not just reads (SP-H): anop: roundtripscenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. TheAllTypesentity (roundtrip-all-types.yaml) carries one field of every persistablefield.*subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an array-of-VOfield.object @isArray @storage:jsonbcolumn (labels, written as 2-element / empty-[]/ single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (field.byte/field.short/field.classwere cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; seefixtures/registry-conformance/README.md→ "Per-subtype write-round-trip matrix".) - API-contract:
fixtures/api-contract-conformance/. TS / C# / Java / Kotlin / Python all 20/20 — each port runs two lanes: a hand-rolled reference server AND its generated API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed. - YAML / verify corpora green across the ports that ship those layers.
Key cross-language features shipped: FR5 family (a/b/c/d/e + WARN envelope-shape — actionable loader errors per ADR-0009); FR-003 (Java RDB runtime persistence + projections; schema migrations are TS-only — the Java migration engine was removed); FR-006 (template.output parser-on-receipt codegen per ADR-0010 in all 5 ports); FR-008 + FR-009 (cross-port REST API contract + 10 filter operators); FR-018 (M:N relationship codegen in all 5 ports — entity navigation + idiomatic ORM wiring [Drizzle m2m / EF Core UsingEntity / Spring repo+JPA / Exposed / Pydantic+route as the SQLAlchemy-secondary equivalent] + REST traversal GET /<source-plural>/{id}/<relation> + Tier-2 docs, gated by the shared api-contract m2m corpus in both lanes + persistence-conformance; the TanStack M:N client hook is a deferred client-ergonomics follow-up); SP-H (field-subtype end-to-end hardening: every concrete field.* subtype write+read round-trips cross-port via the persistence op: roundtrip gate; cut field.byte/field.short/field.class non-functional stubs; cross-port filter-op reconciliation for uuid/currency); source v2 paradigm (ADR-0007); metadata-ktx Kotlin facade; per-target output directories (TS codegen).
Current release line. 0.18.x (npm / NuGet / PyPI) · 7.10.x (Maven Central). The 0.15.0 / 7.7.0 release is a coordinated minor with breaking changes: (1) the metamodel-1.0 vocabulary program — field.uri / field.inet subtypes + a @stringFormat attribute (ADR-0036/0037), field.timestamp instant-by-default with an @localTime naive opt-out, @dbColumnType slim-and-derive (array-ness + text derived; uuid_array/text_array/@kind:text dropped), and reverse-navigation via generated explicit FK finders (ADR-0038); (2) the ADR-0039 own-accessor correctness fix — resolving/effective accessors are the default everywhere, so a concrete field/entity that extends an abstract parent now correctly inherits its properties and members in codegen + runtime (an entire latent bug class, cross-port conformance-gated). Breaking: instant-default timestamps and the @dbColumnType slim. See CHANGELOG.md for the per-version detail. GA promotion is the next release move.
See spec/roadmap.md for the active + planned work picture.
This repository is PUBLIC. Never commit references to other/private projects or to a developer's local environment. In any committed file — specs, plans, code, docs, fixtures — before every commit:
- No other-project names. Do not name private or sibling consumer projects. Use generic terms: "a downstream consumer", "the reference web consumer", "a C# adopter", "a sibling project".
- No absolute local paths. Never commit a developer's home path (e.g.
/home/<user>/…or a~/-rooted path). Use repo-relative paths or placeholders (<repo-root>,<consumer-repo>) in command examples. - Scan the staged diff for both of the above and genericize anything found before committing.
A local pre-commit hook enforces this (.githooks/pre-commit). The committed hook names no private project: it enforces generic structural patterns (absolute home paths) and loads a private-name denylist from a path you configure (kept in a private repo, never here). One-time per clone:
git config core.hooksPath .githooks
git config hooks.denyListPath /path/to/your/private/denylist.txt
It blocks commits whose added lines match (git commit --no-verify bypasses, discouraged); the npm author email is the one allowed exception. Guard a new private name by editing that private denylist (single source of truth) — never add real names to this public repo or the committed hook.
Pre-push typecheck gate (.githooks/pre-push, same core.hooksPath): bun test
transpiles per-file and does NOT typecheck, so type-broken code can ship green on the
test suite while the CI typecheck job goes red — and a direct admin push to main
bypasses branch protection. This hook closes that hole locally: when a push touches
server/typescript/ or client/web/, it runs the same bun run --filter '*' build && … typecheck gate CI runs and blocks the push when it is red (~6s on a clean tree;
skipped entirely for non-TS pushes). Bypass in an emergency with git push --no-verify
or SKIP_TS_TYPECHECK=1 git push. The Java/C#/Python compile+conformance gates do NOT run on PRs (hosted CI runs
them on release tags + manual dispatch only, for cost). Instead, every push to
main triggers local-ci.yml on the maintainer's self-hosted runner: affected
ports only (via scripts/ci-affected-ports.sh), parallel per-port jobs, each
running scripts/ci-local.sh --only <port> --strict-toolchains; a nightly
dispatch runs the full matrix. PRs get the leak-scan only — run
scripts/ci-local.sh --quick locally before opening one.
This repo holds all implementations of the standard, organized by deployment target → language/platform → framework integration:
metaobjects/
├── spec/ # canonical metamodel docs (target-agnostic)
├── fixtures/conformance/ # cross-language test fixtures
│
├── server/ # runs on a server
│ ├── typescript/ java/ python/ csharp/
│
└── client/ # runs on an end-user device
├── web/ # browser (TS-only — the browser is TS-native)
└── ios/ android/ # future
TypeScript plays two distinct roles, and the layout reflects that:
- Server-side TS is a peer port to Java/Python/C# at
server/typescript/. - Universal web client TS at
client/web/is consumed by ALL backends (a Java backend serving React still uses the TS client packages).
Where does a new package go?
- Server-side or client-side? → top-level dir.
- What language/platform? → second-level dir.
- What framework integration? → package name at the third level.
Worked examples: a Drizzle TS-server integration → server/typescript/packages/...; an Angular browser integration → client/web/packages/angular/; future iOS SwiftUI → client/ios/packages/swiftui/.
Server-side (server/typescript/packages/):
metadata/(@metaobjectsdev/metadata) — metamodel loader, types, constantscodegen-ts/(@metaobjectsdev/codegen-ts) — framework-neutral TS codegen engine (entityFile, queriesFile, routesFile, barrel)codegen-ts-react/(@metaobjectsdev/codegen-ts-react) — React codegen (formFile)codegen-ts-tanstack/(@metaobjectsdev/codegen-ts-tanstack) — TanStack codegen (tanstackQuery, tanstackGrid, tanstackGridHook)runtime-ts/(@metaobjectsdev/runtime-ts) — Node-side runtime (Kysely, Drizzle, Fastify helpers)migrate-ts/(@metaobjectsdev/migrate-ts) — migration toolingsdk/(@metaobjectsdev/sdk) — workspace memory, path helperscli/(@metaobjectsdev/cli, binarymeta) — CLI commands:init,gen,migrate
Client-side / universal web (client/web/packages/):
runtime-web/(@metaobjectsdev/runtime-web) — pure framework-agnostic browser core (currency, filter-qs, EntityFetcher contract, GridConfig). Zero React, zero TanStack.react/(@metaobjectsdev/react) — React runtime:useEntityForm,<CurrencyInput>.tanstack/(@metaobjectsdev/tanstack) — TanStack runtime:EntityFetcherProvider,<EntityGrid>, default cell renderers.- Future:
angular/,svelte/,react-native/.
Each framework integration ships as a pair of packages — one for codegen (server-side, runs at meta gen time) and one for runtime (browser-side, runs in the user's app). Mirrors Prisma (prisma + @prisma/client), Apollo (@apollo/codegen-cli + @apollo/client), and Drizzle (drizzle-kit + drizzle-orm).
| Integration | Codegen | Runtime |
|---|---|---|
| React | @metaobjectsdev/codegen-ts-react |
@metaobjectsdev/react |
| TanStack (depends on React) | @metaobjectsdev/codegen-ts-tanstack |
@metaobjectsdev/tanstack |
Each codegen package emits imports that target its matching runtime package. Codegen packages live under server/typescript/packages/ because they execute server-side, even though their output targets the browser. Runtime packages live under client/web/packages/ and have zero Node-only deps.
Two disjoint dependency trees:
Runtime side (browser): Codegen side (server):
@metaobjectsdev/runtime-web ←┐ @metaobjectsdev/codegen-ts ←┐
↑ \ ↑ \
└── @metaobjectsdev/react ┐ ├── @metaobjectsdev/codegen-ts-react
↑ \ └── @metaobjectsdev/codegen-ts-tanstack
└── @metaobjectsdev/tanstack
Future framework integrations (Angular, Svelte, React Native) follow the same two-package pattern.
A user's metaobjects.config.ts:
import { defineConfig } from "@metaobjectsdev/cli";
// Owned generators scaffolded by `meta init` (ADR-0034 scaffold-and-own).
import { entityFile } from "./codegen/generators/entity";
import { queriesFile } from "./codegen/generators/queries";
import { routesFile } from "./codegen/generators/routes";
import { barrel } from "./codegen/generators/barrel";
import { formFile } from "@metaobjectsdev/codegen-ts-react";
import { tanstackQuery, tanstackGrid } from "@metaobjectsdev/codegen-ts-tanstack";
export default defineConfig({
generators: [entityFile(), queriesFile(), routesFile(), formFile(), tanstackQuery(), tanstackGrid(), barrel()],
});A consumer's React component:
import { formatCurrency } from "@metaobjectsdev/runtime-web";
import { CurrencyInput, useEntityForm } from "@metaobjectsdev/react";
import { EntityFetcherProvider, EntityGrid } from "@metaobjectsdev/tanstack";- TS runtime: Bun-first for development (zero-config TS, native test runner). Node-compatible for distribution; users install via npm/pnpm/bun without lock-in to Bun's runtime.
- Module system: ESM only. No CommonJS, no transpile step required.
- Storage format: JSON files in
metaobjects/meta.<concept>.jsonat project root..metaobjects/.gen-state/(gitignored) holds the codegen merge base. - Codegen substrate: ts-poet for greenfield emit, ts-morph for in-place edits, Biome for format pass,
git merge-file --diff3for hand-edit-preserving regen. - Runtime substrate: Kysely for TS (user-provided connection, async-only).
- Migration substrate: Postgres + SQLite for TS v0.3.
- A custom DSL. Plain typed metadata only — Wasp's seven-year DSL-tax is the cautionary tale.
- A spec-driven workflow like Kiro / Spec Kit. Humans don't author rich specs; Claude proposes metadata, humans review.
- A proprietary runtime. All generated code runs without MetaObjects installed; runtime libraries are normal language-native packages.
- A prompt-to-app builder (not Lovable, Bolt, or v0). MetaObjects generates entity-shaped boilerplate; users hand-write the interesting business logic.
- Replacing CLAUDE.md, cursor rules, or other prompt-engineering surfaces — MetaObjects complements them.
- An LLM provider. The MCP integration is model-agnostic.
- An AI agent platform. (Codegen Inc. died Jan 2026 trying that.)
- For new features or non-trivial changes, prefer the brainstorming → plan → implementation flow. Don't jump to implementation without a plan.
- Entity records are prescriptive (drive codegen + runtime). The other record types (decision, principle, convention, glossary, failure) are descriptive (supporting context for reasoning).
- Confidence and provenance are first-class on memory records. Bias toward under-flagging on drift checks (false-positive rate >15% is a kill criterion).
- Templates are user-owned plain TS. Anything inside a generated file is fair game to hand-edit; three-way merge preserves it.
- TDD discipline throughout implementation.
- Cross-language conformance fixtures live at
fixtures/conformance/. Adding new metamodel behavior means adding a conformance fixture so every language port (TS, Java, Python, C#) automatically verifies it. Seespec/conformance-tests.mdfor the fixture format and canonical serializer contract.
Default convention: one file per domain concept under metaobjects/. Multiple objects per file when they share a domain. Projections (source.dbView) live inline with their base entity.
project-root/
├── metaobjects/ # VISIBLE — entity declarations
│ ├── meta.common.json # shared abstracts (BaseEntity)
│ ├── meta.commerce.json # Program, Purchase, ProgramSummary
│ ├── meta.users.json # Subscriber
│ └── meta.content.json # Video, Week, Workout, Exercise
├── .metaobjects/ # HIDDEN — tool state
│ ├── config.json # static project state
│ └── .gen-state/ # codegen merge base (gitignored)
└── metaobjects.config.ts # runtime config
File-naming: meta.<concept>.json. Each file declares its package:
Optional layered overlay pattern (for larger projects with team-level concern boundaries):
metaobjects/
├── meta.user.json # STRUCTURAL (always present)
├── meta.user.ui.json # UI overlay (views, layouts)
└── meta.user.db.json # DB overlay (sources, dbColumns)
All three share the same package and object name. The Loader merges them. Use only when team-level concerns justify the file proliferation. Default to single-file-per-domain.
BaseEntity pattern: shared abstract bases live in meta.common.json. Concrete entities use extends: "BaseEntity" to inherit id + createdAt without redeclaring.
apiPrefix in metaobjects.config.ts flows through codegen to both route registration and hook fetch URLs.
export default defineConfig({
apiPrefix: "/api", // generated routes mount under /api; hooks fetch /api/<entity>
});@metaobjectsdev/codegen-ts follows a Vite-style plugin model.
Core interface — every emitter implements Generator:
import type { Generator, GenContext, EmittedFile } from "@metaobjectsdev/codegen-ts";
interface Generator {
name: string; // kebab-case; surfaces in diagnostics
filter?: (entity: MetaData) => boolean;
generate(ctx: GenContext): EmittedFile[] | Promise<EmittedFile[]>;
}Helpers perEntity() and oncePerRun() cover the common "file per entity" / "one-shot" cases.
Built-in factories: entityFile, queriesFile, routesFile, formFile, barrel. Per ADR-0034 (scaffold-and-own), meta init copies the entityFile/queriesFile/routesFile/barrel reference templates into the consumer repo at codegen/generators/*.ts and the scaffolded config imports those owned local copies; importing them from @metaobjectsdev/codegen-ts/generators still works but is deprecated (removal in a future major).
User wiring (metaobjects.config.ts):
import { defineConfig } from "@metaobjectsdev/cli";
// Owned generators scaffolded by `meta init` (ADR-0034 scaffold-and-own).
import { entityFile } from "./codegen/generators/entity";
import { queriesFile } from "./codegen/generators/queries";
import { routesFile } from "./codegen/generators/routes";
import { barrel } from "./codegen/generators/barrel";
export default defineConfig({
outDir: "packages/database/src/generated",
dialect: "sqlite",
apiPrefix: "/api",
generators: [entityFile(), queriesFile(), routesFile(), barrel()],
});Per-target output directories. Each generator can write to its own
directory/package via a named targets registry + per-generator target, so
generated code lands with its runtime concern (model → database package, routes →
API app, hooks/forms/grids → web app). A target is { outDir, importBase?, outputLayout?, dbImport? }; the top-level outDir is the implicit default
(entity-module) target. Cross-target references to the entity module are emitted as
extension-less importBase package paths (@acme/database/generated/acme/commerce/Program)
while same-target references stay relative; the entity-module target must set
importBase when any generator routes elsewhere. With no targets, output is
byte-identical to a single-outDir project. Full config reference: @metaobjectsdev/cli
README, "Multiple output targets".
Two config files, by design:
metaobjects.config.ts(TypeScript) — generator wiring, type-checked..metaobjects/config.json(JSON) — static project state. Parseable by non-TS tooling (CI scripts, etc.).
The runner runGen() (1) loads metadata, (2) resolves targets + derives the
entity-module target, (3) precomputes shared render state once, (4) runs each
generator with a per-target RenderContext, (5) errors on duplicate full output
paths, unknown target, missing importBase for cross-target imports, or any
generator throw, (6) writes each file under its target's outDir (overwriting only
files carrying the @generated header; refusing others).
Generated CRUD endpoints support a typed, metadata-driven filter + sort layer:
URL grammar (bracketed qs): ?filter[field][op]=value&sort=field:asc|desc&limit=N&offset=N. Bare value is sugar for eq.
Eight operators, gated by field subtype:
eq,ne,gt,gte,lt,lte,in,like,isNull- Strings get
eq/ne/in/like/isNull; numbers + dates geteq/ne/gt/gte/lt/lte/in/isNull; booleans geteq/isNull.
Authoring: mark fields with @filterable: true. @sortable inherits from @filterable by default.
Generated artifacts per entity:
<Entity>FilterAllowlist— server-side allowlist<Entity>SortAllowlist— server-side sort allowlist<Entity>Filter— client TS filter type
Client usage:
import { useSubscribers } from "./generated/Subscriber.hooks";
const { data } = useSubscribers({
email: { like: "%@example.com" },
subscribed: true,
sort: "createdAt:desc",
limit: 25,
});Server validation: every request validated against the allowlist. Unknown field / disallowed op / invalid value → 400 with structured error code.
Architecture: parseFilterParams (in @metaobjectsdev/runtime-ts/drizzle-fastify) translates parsed qs into a Drizzle expression tree. buildFilterQs (in @metaobjectsdev/runtime-web and @metaobjectsdev/tanstack) serializes a typed filter object back to a bracketed qs URL.
source is a top-level metadata type describing where an object's data lives. Subtypes: dbTable (writable, default) and dbView (read-only).
Authoring a projection:
{ "object.entity": {
"name": "ProgramSummary",
"extends": "Program",
"children": [
{ "source.dbView": { "@name": "v_program_summary" }},
{ "field.int": { "name": "weekCount", "children": [
{ "origin.aggregate": {
"@agg": "count", "@of": "Week.id", "@via": "Program.weeks" }}
]}},
{ "identity.primary": { "@fields": ["id"] }}
]
}}origin subtypes: passthrough (cross-entity field reference) and aggregate (count/sum/avg/min/max). Origins drive view DDL.
Source-aware codegen dispatch:
- Projection (dbView only) → read-only Zod, read-only routes, read-only hooks.
- Write-through (dbTable + dbView) → mutations target table, queries target view.
- Vanilla entity → standard behavior.
columnNamingStrategy in metaobjects.config.ts: snake_case (default) | literal | kebab-case.
field.currency declares "this column stores money as integer minor units."
{ "field.currency": {
"name": "priceCents",
"@currency": "USD",
"children": [
{ "view.currency": { "@locale": "en-US" }}
]
}}Storage: integer minor units (cents for USD, yen for JPY). Wire format is unchanged from long. Server never formats currency; all formatting is client-side via Intl.NumberFormat.
Runtime imports (browser-safe sub-paths):
import { formatCurrency, parseCurrency } from "@metaobjectsdev/runtime-web";
import { CurrencyInput } from "@metaobjectsdev/react";Cross-language ports must preserve the wire contract: integer minor-unit storage, @currency (ISO 4217), @locale (BCP 47) attrs.
@metaobjectsdev/codegen-ts-tanstack ships two generators:
tanstackQuery()— emits<Entity>.hooks.tsper entity (5 hooks:useEntity,useEntities,useCreate/Update/Delete<Entity>).tanstackGrid()— emits<Entity>.columns.tsxper entity with alayout.dataGridchild.
Grid metadata:
{ "layout.dataGrid": {
"name": "default",
"@columns": ["email", "firstName", "subscribed", "createdAt"],
"@defaultSortField": "createdAt",
"@defaultSortOrder": "desc",
"@pageSize": 25
}}Runtime surface (@metaobjectsdev/runtime-web and @metaobjectsdev/tanstack):
<EntityFetcherProvider value={fetcher}>— supplies the fetcher function.<CellRendererProvider value={{...}}>— renderer overrides keyed by view subtype.<EntityGrid columns={...} grid={...} data={...} />— opinionated TanStack Table component.
Per-entity opt-out: @emitTanstack: false on an entity skips both hook and column files.
Preserve the following contracts exactly across all language ports:
Metamodel subtype vocabularies (must be identical across languages): the registry-conformance gate (fixtures/registry-conformance/) is the structural enforcer of this rule — each port emits its registry as a canonical manifest byte-matched to expected-registry.json. All five ports (TS / C# / Java / Kotlin / Python) are live + green (SP-G Java/Kotlin reconciliation complete; the JVM runners compose from the defined metamodel provider set so codegen-base/om classpath SPI does not pollute the measured vocabulary). See fixtures/registry-conformance/README.md.
- Filter operators:
eq,ne,gt,gte,lt,lte,in,like,isNull - Object subtypes:
entity(owns data: own identity, writable sources, lifecycle),value(pure shape: NO identity, NO source, ever; constructed — by caller/assembly/embedding — never populated; mayextendsentity fields for shape),projection(derived read-only representation: fieldsextends-bound / origin-derived / self-declared-under-external-assembly, all read-only at subtype level; identity optional and MUST extend an entity identity; sources restricted to read-only@kinds; the declared field set IS the exposure — inclusive list, fail-closed). A field carryingorigin.*is derived ⇒ read-only wherever it lives (incl. on entities). An entity's primary source must be a writable@kind(read-only kinds only in read role). See ADR-0028. (FR-024 Phase E —object.projection/valueare registered inexpected-registry.jsonand the projection/value validation passes [identity pass-through, value-purity, projection-licensing,@viainference/cardinality, extends/origin agreement, derived-field providability] are enforced cross-port in all 5 ports. The B4b entity-primary-source-readonly cutover [the "writable@kind" clause above —ERR_ENTITY_PRIMARY_SOURCE_READONLY] + the projection codegen fan-out (read-only DTOs for view-kind projections; FR-015 proc-callables for proc-kind projections; api-docs labelobject.projectionunits asprojectionand document their generated<Name>Dto) are now shipped cross-port; the remaining FR-024 work is the declared-API surface — tracked in #10.) - Source subtypes:
rdb(paradigm; ADR-0007). The pre-v2dbTable/dbViewsubtypes are RETIRED —source.rdb+@kind: table|view|materializedView|storedProc|tableFunctionis the form, with read-only-ness derived from@kind. Multi-source via@role(exactly oneprimaryper object). Source physical name =@table(NOT@name); field physical name =@column(renamed from@dbColumn). Referential actions on relationships:@onDelete/@onUpdate. - Origin subtypes:
passthrough,aggregate - Relationship subtypes:
association,aggregation,composition. Cardinality via@cardinality: one|many; target via@objectRef. M:N (FR-018) slim vocabulary:@cardinality: "many"+@objectRef(target) +@through(the junction/through entity — a third entity that MUST declare twoidentity.referencechildren, one per FK side). The relationship's FK fields are derived from those references (theidentity.referenceSSOT for FK direction), never restated.@sourceRefField(optional) disambiguates a directed self-join by naming the source-side FK field on the junction (the other reference is the target side).@symmetric(optional boolean) marks an undirected self-join (union-on-read) — valid only when@objectRef== the declaring entity, and mutually exclusive with@sourceRefField. The pre-FR-018@joinEntity/@joinFieldsattrs are REMOVED. Validation errors: symmetric-on-hetero / symmetric+sourceRefField →ERR_BAD_ATTR_VALUE; junction-missing-two-references / sourceRefField-not-matching / M:N-attr-on-1:N →ERR_INVALID_RELATIONSHIP. - Index subtypes:
index.lookup(non-unique retrieval index; uniqueness is encoded in the type:identity.secondary= unique alternate key,index.lookup= non-unique;@uniqueis REMOVED fromidentity.secondary—ERR_UNKNOWN_ATTRon any legacy@unique). RDB-physical escapes@using/@expr/@where/@ordersare registered by the db provider on bothidentity.secondaryandindex.lookup.index.fulltext/index.vector/index.spatialare reserved on the subtype axis — documented, NOT registered (YAGNI + 1.0 vocab freeze). See ADR-0040. - Layout subtypes:
dataGrid - API subtypes:
api.base/api.operational(request/response surface; subtype axis = interaction model, NEVER protocol — protocol lives inbinding.*per operation:restnow,messaging/grpcreserved). Children:operation.query(outputRef →object.projection) /operation.command(inputRef →object.value, may also outputRef). Derived CRUD (FR-008/009) stays the zero-config default; declaredapiextends it. Org-tier modeling (application/service/network/deployment) stays OUT of core — provider SPI, FQN references. See ADR-0030. (FR-024 declared-API — planned; not yet inexpected-registry.json; the remaining third of FR-024 after the projection/value taxonomy + validation parity.) - Currency attrs:
@currency(ISO 4217),@locale(BCP 47) - Schema attrs:
@schemaonsource.rdb(DB schema name; Postgres defaultpublic, SQLite rejects non-default values) - Storage attrs:
@storageonfield.object(with@objectRef) — valuesflattened/jsonb/subdocument. Unifies "owned types" (flattened storage) and "structured JSONB" (jsonb storage). Defaults to single-jsonb-column when absent (back-compat). - Enum:
field.enumis a first-class field subtype (peer ofcurrency), string-backed. Required@valuesstring-array attr (member symbols). Members must be a non-empty set, each matching^[A-Za-z_][A-Za-z0-9_]*$, no duplicates — every port's loader enforces this (own-only) emittingERR_BAD_ATTR_VALUE(missing@values→ERR_MISSING_REQUIRED_ATTR). Reuse via abstractfield.enum+extends. Codegen: TS union +z.enum, C#enum+ EFHasConversion<string>(), DBvarchar+CHECK. Int-backed values, display labels, and native PG enum are deferred (seedocs/superpowers/specs/2026-05-23-enum-datatype-design.md). - Documentation common attrs (any node):
description,title,notes,deprecated,replacedBy,seeAlso,aliases. Registered via the cross-languagecommonAttrsregistry hook (registerCommonAttrs/RegisterCommonAttrs/register_common_attrs/ JavaMetaDataRegistry.registerCommonAttribute— wired in all four ports).notesis the internal-only rationale slot — never emitted to user-facing doc-gen (JSDoc / XML-doc / PostgresCOMMENT ON/ Mermaid prose). TS doc-gen ships all three tiers; C# ships XML-doc + COMMENT ON. Seedocs/superpowers/specs/2026-05-24-documentation-provider-design.md.
Wire format:
- Currency: integer minor units on the wire always. Float arithmetic for money is forbidden.
- Pagination:
?limit=N&offset=N— identical across all generated endpoints. - Runtime return types: a port's runtime
ObjectManagerreturns native in-process language types (field.decimal→BigDecimal/decimal/Decimal, TSstring; temporal→native; jsonb→native map). Wire canonicalization (the bullets above +normalization.md) is applied at the serialization boundary, never inside the runtime query path. See ADR-0019.
Grammar:
- Dotted-path syntax for
@via:"Program.weeks"or"Program.weeks.workouts". - Dotted-path syntax for
@of:"Week.id". extendsmay target a nested child to ANY depth:Customer.id, cross-packageacme::sales::Customer.id, triple-nestCustomer.priceCents.display(object → field → view). Addressing model: a package qualifies the ROOT-level node only; each subsequent dotted segment traverses CHILD NAMES (nested children carry BARE names — packages never fold onto non-root nodes). Intermediate segments resolve by unique name (cross-type collision = unresolved); the FINAL segment is type-scoped to the referrer (a field resolves fields; an identity identities; a view views).extendsis THE inheritance mechanism;origin.*never inherits.@vialives onorigin.*only and may be omitted ONLY when exactly one single-hop relationship leads from the base entity to thefrom/ofentity (multi-hop always explicit). See ADR-0029.- Package segments:
::separator —acme::common::id. - Canonical JSON: reserved structural keywords are bare (
name/package/extends/abstract/overlay/isArray/children/value); inline attributes are@-prefixed.@-prefixing a reserved word (e.g.@isArray) is invalid (ERR_RESERVED_ATTR). YAML authoring is sigil-free — bare attrs; the desugar re-adds@when lowering to canonical JSON; canonical JSON is the on-disk interchange (YAML is the universal authoring front-end across ports). See ADR-0006.
Loader pipeline:
extends:resolution happens after all files are loaded (deferred, not eager).- Overlay/merge: same
package+ same objectnameacross multiple files → merged. Last-writer-wins on attr conflicts; structural children accumulate. - Default scan path:
metaobjects/**/*.json(recursive).
D1 is TS-only. Cloudflare D1 is a peer of sqlite/postgres in TS's dialect vocabulary. It is SQLite at the SQL level — Java/Python/C# don't have an analogue (Cloudflare Workers run JS). When adding cross-language vocabulary, D1 doesn't constrain anything: its uniqueness is wrangler-CLI transport + Wrangler-native file layout (migrations/<seq>_<slug>.sql), both of which are TS-only concerns.
Constants discipline:
- TS: named constants in
packages/metadata/src/constants.ts. Never inline metamodel strings as literals in code. - New type or subtype names: add to TS constants first; add the parallel in other language implementations.
These are the load-bearing principles that have emerged through implementation. Apply them every time.
-
Expanding the metamodel vocabulary follows ONE decision procedure (ADR-0037) — driven by semantic behavior, not surface storage. Don't ask "is X a string/number/date?"; ask "what does X do?" Ordered test: (0) derivable from existing subtype + attrs (
isArray/@maxLength) + structure? → derive in codegen, add nothing; (1) physical-only (native type/meaning unchanged)? → the@dbColumnTypeescape hatch, not first-class vocab; (2) logical — does X have its own native type, behavior, or attributes (a thing that owns custom logic)? → subtype (the extension point:field.uuid,field.uri,field.inet); a structural variant within a subtype (changes generated shape, shares native type)? →@kind(the one chartered structural-variant axis: source table/view, uri url/urn — never a catch-all, never on a plain string); otherwise X just modifies/validates/configures an existing type → attribute (boolean flag@localTime; validation@stringFormatemail/hostname; config@maxLength). The "string formats" set splits by behavior: url/uri→field.uri, ip→field.inet(native types + behavior), only email/hostname→@stringFormat(plain validated strings); uuid is alreadyfield.uuid. Self-documentation over economy; no same-name overloads (hence@stringFormat, not a third@format). Consult it every time. See ADR-0037. -
Pattern-derivable from metadata = codegen, never hand-code. This is the metaobjects raison d'être. If you find yourself proposing that users hand-write something the metadata fully describes (FK references, basic CRUD, validator chains, type-safe finders, relations() blocks), stop. Codegen it. The only exception is what metadata genuinely cannot express (custom SQL views, regex patterns from outside metadata, business logic). When in doubt, generate.
-
Study reference implementations for subtle pipeline behavior; don't re-derive from spec. For complex orchestration (loader, parser, super resolution, overlay/override merging, registry lifecycle), the spec describes WHAT but the implementation captures HOW — including edge cases and error handling. When porting, read existing implementations first. First-principles reasoning produces subtly wrong behavior that breaks cross-language interop.
-
"Validated by spike" ≠ "right design". A spike proves a technique works under a specific test. It doesn't prove it is the best production choice. Always ask "what's the UX cost?" alongside "does this work?"
-
NEVER call
own*()accessors by default — resolving/effective is the default;ownbreaksextends(ADR-0039).extendsis a super-reference, not a flatten: inherited attrs/children live on the parent, reachable only via the resolving accessors (attr()/children()/getMetaAttr(name)/ Pythonattrs().get()). Reading a field/node's effective property (isArray,subType,@maxLength,@precision,@default,@column,@objectRef,@storage, …) or iterating its member set through an own-only accessor (ownAttr/ownChildren/ownFields/field.isArraynative flag / Java,false) silently drops everything inherited viaextends— corrupting codegen and runtime. The one legitimateown*()use: codegen emitting a generated subclass, iteratingownFields()so inherited members aren't re-emitted (the generated base already has them). The metamodel-internal siblings (own-mode canonical serializer, overlay-merge, super-resolution walks) use the same "emit only the declared-here layer" principle. The one attr deliberately own-only is@dbColumnType(physical, never inherited). Everyown*()call must carry a comment naming its sanctioned case; any other own read is a bug. Watch the naming inversion: Pythonattr()is OWN, TSattr()resolves. See ADR-0039. -
Bind metadata→native types at build time, never runtime reflection. Resolving an object's native class/module from its FQN must happen in generated code (static imports for data-oriented ports; a domain-sliced, FQN-keyed registry for OO ports), not via
Class.forName/Type.GetType/importlib— runtime reflection is impossible in TS and breaks under GraalVM native-image / .NET AOT. See ADR-0001. -
Record significant cross-cutting decisions as ADRs. Durable, cross-language/cross-feature architectural contracts live in
spec/decisions/(Nygard format). Consult them before changing a cross-language contract; add a new ADR when you make one. Feature-level decisions stay in the FR spec; this file holds only the one-line rule + the pointer. -
Strict metadata provenance — never invent a metamodel attribute (ADR-0023). Every type/subtype/attribute the loader accepts in THIS repo must come from a registered metamodel provider; the library boots strict + seals the registry after the defined-provider bootstrap, so any undeclared attr is
ERR_UNKNOWN_ATTRand any post-bootstrap registration isERR_REGISTRY_SEALED(codegen "making up" an attr is a hard build failure). Before adding ANY new metamodel attribute: (1) prove it cannot be computed from existing metadata — if a generator can derive it (FK refs, column types, validator chains, naming), derive it, don't add an attr; (2) get explicit human agreement and write the can't-be-computed justification into the FR spec / ADR; (3) add it to a registered provider AND aregistry-conformancefixture so all five ports gate it. Downstream apps may add their own providers or loosen strictness — but the library's tests never permit an unregistered attr. See ADR-0023. The legitimate escape hatch for arbitrary author-supplied properties is the registeredattr.propertiesbag (exempt from the strict-attr check), not a new first-class attr.
- Named constants for metamodel strings — always. Type names, subtype names, reserved JSON keys, special attribute names, structural separators, and wildcards live in
packages/metadata/src/constants.ts— import and use them. Gets you compile-time typo safety. - Use
as constarrays + type unions for closed sets (e.g.,FIELD_SUBTYPES = [...] as const; type FieldSubType = (typeof FIELD_SUBTYPES)[number]). - String literals OK only for: error message text, instance/entity names that are user data, and test data values that aren't metamodel-level concepts.
- No backwards-compat hacks.
- No
anyescape hatches. Useunknownand narrow.
meta init # scaffold metaobjects/, .metaobjects/, codegen/generators/, metaobjects.config.ts, .gitignore
meta gen [<entity>...] # codegen: render templates → format → three-way merge → write
meta gen --dry-run # preview without writing
meta migrate # diff metadata vs DB schema; emit migration SQL
meta migrate --dry-run # preview without writing migration file
The above is the Node meta (schema + TS codegen). Each non-TS port runs
codegen through its own build tool — dotnet meta gen/verify (C#),
mvn metaobjects:generate/:verify (Java/Kotlin), metaobjects gen/verify
(Python). Schema (migrate, verify --db) is Node-meta-only. Full matrix +
rationale: docs/features/cli.md (locked CLI architecture,
ADR-0015).
The Bun workspace root is the repository root (/package.json), which globs server/typescript/packages/* and client/web/packages/*. Java/Python/C# live outside the JS workspace (not globbed). Run bun install once at the repo root. Run bun test scoped — cd server/typescript && bun test for the server suite (this also picks up server/typescript/bunfig.toml's test preload), and per-package for client/web. Never run a bare bun test at the repo root: it walks java/, python/, csharp/, and fixtures/ looking for test files, turning a fast per-package run into many minutes.
bun install # once, at the repo root
cd server/typescript && bun test # server suite (per-package; a bare run over the whole server suite now exceeds 5 min — scope it)
cd client/web/packages/<pkg> && bun test # a single client package
bun run --filter '*' typecheck # whole workspace, from repo root
bun run --filter '*' build # whole workspace, from repo root
PRs welcome. When contributing:
- Follow the TDD discipline: write tests first, then implementation.
- Use named constants for all metamodel strings — never inline
"field","object", etc. - No
any— useunknownand narrow. - Run
bun testin the relevant package before opening a PR. All tests must pass. - For cross-language changes, ensure the wire format and vocabulary are preserved exactly.
- Look at existing generator implementations before adding a new one — the pattern is intentionally consistent.
For significant new features or architectural changes, open an issue first to discuss the approach.
Publishing to npm: see docs/RELEASING.md — the procedure (RC → smoke-test → promote) plus the non-obvious gotchas (publish with bun, regen the lockfile after every version bump, runtime imports must be dependencies, verify a real external install in npm and pnpm).
See spec/roadmap.md for current and planned library work. (Consumer-adoption validation and the application-level prompt-pillar consolidation are pursued in adopter projects, not tracked in this repo.)
- [TECHNICAL] Field-type → Drizzle-column-type mapping table (needed for complete TS codegen coverage).
- [TECHNICAL] ObjectManagerDB further modernization. FR-003 Plan 4 (2026-05-27) closed the three engine-debt anti-patterns. The Spring Boot 3 starter + OMDB autoconfiguration + virtual-thread audit shipped 2026-05-30 (
metaobjects-spring-boot-starter). jOOQ migration is a closed non-goal: jOOQ's OSS edition excludes Oracle/SQL Server/DB2 (commercial license required), which would paywall OMDB's commercial-DB drivers in a public OSS project, and jOOQ generates code from a schema — the inverse of MetaObjects' metadata-is-the-spine model. - [TECHNICAL] Payload
origin.*resolution incodegen-spring(Day-1 deferral — seeserver/java/codegen-spring/src/main/java/com/metaobjects/generator/spring/KNOWN_GAPS.md). Kotlin'sKotlinPayloadGeneratoris the cross-port reference. - [TECHNICAL] WARN envelope-shape assertion on cross-port
expected-warnings.json(closed 2026-05-27 — runners now assert envelope shape on warnings; legacy string-list path retired).
{ "metadata.root": { "package": "myapp::commerce", "children": [ { "object.entity": { "name": "Program", ... }}, { "object.entity": { "name": "Purchase", ... }} ] }}