Skip to content

Commit fe93553

Browse files
committed
docs(getting-started): match the 1.0.9 walkthrough
Install @metaobjectsdev/metadata and qs (plus @types/qs) instead of runtime-ts, start from npm init -y, and describe the ejected routes' owned HTTP adapter. The output is verified against @metaobjectsdev/cli@1.0.9. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M1mRRB1K2WdYy82kntMQdx
1 parent 5f8c105 commit fe93553

1 file changed

Lines changed: 44 additions & 25 deletions

File tree

‎www/getting-started.html‎

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,17 @@ <h1>One typed model → idiomatic code, in your language.</h1>
5151
for tasks,"</em> <em>"add a priority field and update everything."</em> Because <code>meta&nbsp;init</code> installs
5252
the MetaObjects skills, it knows the model is the source of truth: it edits the model, picks the generators the
5353
job needs, and <strong>generates the code</strong> — it never hand-writes the boilerplate. Every command and
54-
output below is real <span class="real">verified</span>, run against <code>@metaobjectsdev/cli@1.0.7</code>.</p>
54+
output below is real <span class="real">verified</span>, run against <code>@metaobjectsdev/cli@1.0.9</code>.</p>
5555
</section>
5656

5757
<section>
5858
<h2 class="section-label">0 · Already have an app? Assess it first — nothing to install</h2>
5959
<p class="gs-note">Adopting into an existing codebase? Before you install anything, have your agent run the
60-
<strong>fit &amp; migration assessment</strong> against your repo. It's read-only and propose-only — it reads
61-
your code, migrations, and git history, and writes a decision-grade report: per-pillar fit verdicts (including
62-
<em>NOT A FIT</em>), a drift ledger of the shapes you already declare twice — with the past commits where a fix
63-
patched one copy and missed the other — and, if the verdict is yes, a first-week wedge plan.</p>
60+
<strong>fit &amp; migration assessment</strong> against your repo. It's read-only and propose-only. A quick pass (5–30 minutes of agent time) reads your
61+
code, migrations and git history and answers in the conversation: how much of MetaObjects this project should
62+
adopt — <em>not worth it</em>, a contract spine between your apps, one subsystem, or all of it — and the seams
63+
where it would pay off. Ask for the full assessment and it also writes an evidence-cited report, outside your
64+
repo, with a drift ledger of the shapes you already declare twice and a first-week plan.</p>
6465
<p class="gs-say">Fetch https://metaobjects.dev/assess.md and run the MetaObjects Fit &amp; Migration Assessment against this repository.</p>
6566
<p class="gs-note">Any agent that can fetch a URL works (Claude Code, Cursor, Windsurf, Copilot's
6667
<code>#fetch</code>, Gemini CLI). Locked-down agent? Save <a href="/assess.md">the prompt file</a> into your
@@ -75,7 +76,8 @@ <h2 class="section-label">1 · Install &amp; scaffold</h2>
7576
<div class="gs-step">
7677
<h3>Set up the project and the agent context</h3>
7778
<p>Add the TypeScript CLI and scaffold the workspace. <code>meta init</code> also teaches your coding agent how to use MetaObjects:</p>
78-
<pre class="gs-code">npm i <span class="s">@metaobjectsdev/cli</span>
79+
<pre class="gs-code">npm init -y
80+
npm i <span class="s">@metaobjectsdev/cli</span>
7981
npx meta init</pre>
8082
<p>That writes a small, legible tree — <strong>your models go in <code>metaobjects/</code></strong>,
8183
and <code>codegen/generators/</code> is waiting for the generators you choose:</p>
@@ -112,7 +114,7 @@ <h2 class="section-label">2 · Then just tell it what you want</h2>
112114
<div class="gs-steps">
113115
<div class="gs-step">
114116
<h3>Author the model</h3>
115-
<p class="gs-say">Model a Task — a title (required, max 120), a status of open / active / done, and when it was created.</p>
117+
<p class="gs-say">Model a Task — a title (required, max 120), a status of open / active / done, and a created-at time the server sets itself.</p>
116118
<p>→ Claude writes <code>metaobjects/meta.tasks.yaml</code> using the <code>metaobjects-authoring</code> skill. You review a few readable lines.</p>
117119
</div>
118120
<div class="gs-step">
@@ -170,8 +172,11 @@ <h2 class="section-label">4 · Author a model</h2>
170172
- <span class="k">field.long</span>: { name: id }
171173
- <span class="k">field.string</span>: { name: title, required: true, maxLength: 120 }
172174
- <span class="k">field.enum</span>: { name: status, values: [<span class="s">open</span>, <span class="s">active</span>, <span class="s">done</span>] }
173-
- <span class="k">field.timestamp</span>: { name: createdAt, required: true }
175+
- <span class="k">field.timestamp</span>: { name: createdAt, required: true, autoSet: onCreate }
174176
- <span class="k">identity.primary</span>: { fields: [id], generation: increment }</pre>
177+
<p class="gs-note"><code>autoSet: onCreate</code> makes <code>createdAt</code> the server's job: the generated
178+
insert schema stamps it with the current time, so a client never sends it, and a value a client does send is
179+
ignored.</p>
175180
</section>
176181

177182
<section>
@@ -197,22 +202,28 @@ <h2 class="section-label">5 · Choose your generators</h2>
197202
requires: entity
198203
routes-hono — Per-entity Hono CRUD routes (runtime-ts/hono mountCrudRoutes). [hono, would emit 1]
199204
<span class="c"># … client, docs and capability generators, then the ai and iam libraries</span></pre>
205+
<p class="gs-note">That is the layout you see in a terminal. When stdout is not a terminal (piped, or run by an
206+
agent), <code>meta gen --list</code> prints a structured format instead; pass <code>--format text</code> to get
207+
the layout above, or <code>--format json</code> for JSON. <code>meta eject</code> behaves the same way.</p>
200208
<p class="gs-note">Take the ones you want. <code>meta eject</code> copies each into
201209
<code>codegen/generators/</code> and tells you exactly how to wire it:</p>
202210
<pre class="gs-code">$ npx meta eject entity queries routes names barrel
203211
Ejected "entity" -> codegen/generators/entity.ts. You own it now (ADR-0034 scaffold-and-own).
204212
In metaobjects.config.ts, "entityFile" must resolve to this file:
205213
import { entityFile } from "./codegen/generators/entity.js";
206-
<span class="c"># … the same for queries, routes, names and barrel</span>
214+
<span class="c"># … the same for queries, routes, names and barrel; for routes also:</span>
215+
The code this generator emits imports its HTTP adapter from codegen/runtime/, not from @metaobjectsdev/runtime-ts: the adapter, filter parser, error envelopes and pagination are yours too.
207216

208217
Install what the ejected generators and their output need:
209218
<span class="c"> # … the packages below</span>
210219
These generators read config: apiPrefix, collectionNameOverrides, columnNamingStrategy, dbImport, dialect, extStyle, …</pre>
211220
<p class="gs-note">Those five files are the point: they're plain TypeScript in your repo, and <strong>yours to
212221
edit</strong>. <code>meta gen</code> runs those local copies — not the ones inside the package — so changing
213-
the shape of the generated code is an ordinary edit to a file you own. Install what they import:</p>
214-
<pre class="gs-code">npm i -D <span class="s">@metaobjectsdev/codegen-ts</span> <span class="s">typescript</span>
215-
npm i <span class="s">@metaobjectsdev/runtime-ts</span> <span class="s">"drizzle-orm@&gt;=0.36.0 &lt;1.0.0"</span> <span class="s">"fastify@&gt;=5.0.0 &lt;6.0.0"</span> <span class="s">"zod@&gt;=3.23.0 &lt;5.0.0"</span></pre>
222+
the shape of the generated code is an ordinary edit to a file you own. Ejecting <code>routes</code> also copies
223+
the HTTP adapter the routes call (mount helpers, filter parser, error envelopes, pagination) into
224+
<code>codegen/runtime/</code>, so that code is yours too. Install what they all import:</p>
225+
<pre class="gs-code">npm i -D <span class="s">@metaobjectsdev/codegen-ts</span> <span class="s">@types/qs</span> <span class="s">typescript</span>
226+
npm i <span class="s">@metaobjectsdev/metadata</span> <span class="s">qs</span> <span class="s">"drizzle-orm@&gt;=0.36.0 &lt;1.0.0"</span> <span class="s">"fastify@&gt;=5.0.0 &lt;6.0.0"</span> <span class="s">"zod@&gt;=3.23.0 &lt;5.0.0"</span></pre>
216227
<p class="gs-note">Then add them to <code>metaobjects.config.ts</code>, with the config keys the routes need. Here's
217228
the file with <code>meta init</code>'s longer comments trimmed:</p>
218229
<pre class="gs-code"><span class="c">// metaobjects.config.ts</span>
@@ -272,21 +283,21 @@ <h2 class="section-label">6 · Generate</h2>
272283
status: text(TaskNames.fields.status.column, {
273284
enum: [<span class="s">"open"</span>, <span class="s">"active"</span>, <span class="s">"done"</span>] as const,
274285
}),
275-
createdAt: text(TaskNames.fields.createdAt.column).notNull(),
286+
createdAt: text(TaskNames.fields.createdAt.column)
287+
.notNull()
288+
.$defaultFn(() =&gt; new Date().toISOString()),
276289
},
277290
<span class="c">// … plus a CHECK constraint on status</span>
278291
);
279292

280293
export const TaskInsertSchema = z.object({
281294
title: z.string().min(1).max(120),
282295
status: z.enum([<span class="s">"open"</span>, <span class="s">"active"</span>, <span class="s">"done"</span>]).optional(),
283-
createdAt: z.string(),
296+
createdAt: z
297+
.string()
298+
.optional()
299+
.transform(() =&gt; new Date().toISOString()),
284300
});</pre>
285-
<p class="gs-note">To typecheck what was generated, create a <code>tsconfig.json</code> once and run the compiler.
286-
<code>meta init</code> only scaffolds <code>tsconfig.codegen.json</code>, which covers the generators you own,
287-
not <code>src/</code>:</p>
288-
<pre class="gs-code">npx tsc --init
289-
npx tsc --noEmit</pre>
290301
</section>
291302

292303
<section>
@@ -316,28 +327,36 @@ <h2 class="section-label">7 · Wire your database</h2>
316327
.metaobjects/migrations/&lt;timestamp&gt;-init/down.sql
317328

318329
migrate: applied 1 migration(s): &lt;timestamp&gt;-init</pre>
330+
<p class="gs-note">Now that <code>src/db.ts</code> exists, typecheck everything: create a <code>tsconfig.json</code> once and run the compiler.
331+
<code>meta init</code> only scaffolds <code>tsconfig.codegen.json</code>, which covers the generators you own,
332+
not <code>src/</code>:</p>
333+
<pre class="gs-code">npx tsc --init
334+
npx tsc --noEmit</pre>
319335
</section>
320336

321337
<section>
322338
<h2 class="section-label">8 · Use it — and add your own logic</h2>
323339
<p class="gs-note">Import the generated code into your app:</p>
324-
<pre class="gs-code"><span class="c">// src/server.ts — run it with: npx tsx src/server.ts</span>
340+
<pre class="gs-code"><span class="c">// src/server.ts — run it with: npx tsx src/server.ts (npx fetches tsx on first use)</span>
325341
import Fastify from <span class="s">"fastify"</span>;
326342
import { taskRoutes } from <span class="s">"./generated/Task.routes.js"</span>;
327343

328344
const app = Fastify();
329345
app.register(taskRoutes); <span class="c">// GET/POST/PATCH/DELETE /tasks, validated</span>
330346
await app.listen({ port: 3000 });</pre>
331347
<pre class="gs-code">$ curl -X POST localhost:3000/tasks -H <span class="s">'content-type: application/json'</span> \
332-
-d <span class="s">'{"title":"Write the page","status":"open","createdAt":"2026-09-14T12:00:00Z"}'</span>
333-
{"id":1,"title":"Write the page","status":"open","createdAt":"2026-09-14T12:00:00Z"}
348+
-d <span class="s">'{"title":"Write the page","status":"open"}'</span>
349+
{"id":1,"title":"Write the page","status":"open","createdAt":"2026-09-26T14:08:05.053Z"}
334350

335351
$ curl -X POST localhost:3000/tasks -H <span class="s">'content-type: application/json'</span> -d <span class="s">'{"status":"nope"}'</span>
336352
{"error":"validation","issues":[{"expected":"string","code":"invalid_type","path":["title"], <span class="c">…</span></pre>
337-
<p class="gs-note">The entity module imports only Drizzle and Zod. The routes mount through
338-
<code>@metaobjectsdev/runtime-ts</code>, an ordinary Apache-2.0 package you could vendor. <strong>The generated
353+
<p class="gs-note">The entity module imports only Drizzle and Zod. The routes mount through the
354+
HTTP adapter eject copied into <code>codegen/runtime/</code>: plain TypeScript you own and can change. <strong>The generated
339355
endpoints are unauthenticated:</strong> register them inside a Fastify scope that carries your auth hook. The
340356
header of <code>Task.routes.ts</code> shows how.</p>
357+
<p class="gs-note">Lists filter as <code>?filter[status][eq]=open</code>, and only fields you mark
358+
<code>filterable: true</code> in the model accept a filter; filtering on any other field answers 400
359+
<code>invalid_filter_field</code>. That keeps an unindexed column from becoming a query anyone can run.</p>
341360
<p class="gs-note">The custom business logic that makes the app yours? You write it, in your own modules, against
342361
the generated, typed code. A <code>Task.extra.ts</code> beside the output is a naming convention, not a plugin
343362
point: nothing imports it for you, so register its handlers next to <code>taskRoutes</code>. Change the model,
@@ -349,7 +368,7 @@ <h2 class="section-label">8 · Use it — and add your own logic</h2>
349368
<h2 class="section-label">9 · Keep it honest</h2>
350369
<p class="gs-note">As the model evolves, <code>meta verify</code> fails the build the moment generated code, the
351370
database schema, or a prompt template drifts from it:</p>
352-
<pre class="gs-code">npx meta verify <span class="c"># prompt templates + the requirements ledger</span>
371+
<pre class="gs-code">npx meta verify <span class="c"># the prompt-template gate (the default when no gate is named)</span>
353372
npx meta verify --codegen <span class="c"># generated code vs the model</span>
354373
npx meta verify --db file:dev.sqlite <span class="c"># live database schema vs the model</span></pre>
355374
<p class="gs-note">Using a different assistant than Claude Code? Point it at

0 commit comments

Comments
 (0)