Run it in your browser
Every other chapter of this guide shows an application that was generated by a command and run on a machine. This one skips both. Give it a Mermaid model and it compiles that model into a complete application — schema, dictionary, rules, processes, access control and an interface — and then runs it, here, with no server involved at any point.
Three things make that possible, and none of them is a simulation. The database is
PostgreSQL 18 compiled to WebAssembly, so the schema your model describes is the schema that
actually runs: real types, real constraints, real transactions. The application server is the
same server/ code the command-line generator writes, running on a worker thread under a
Node-API runtime. And a Service Worker answers the page's own /api requests, so
the interface makes ordinary HTTP calls to its own origin and something else entirely replies —
exactly as it would against a real server, which is why pointing this application at one later is a
deletion rather than a rewrite.
And it arrives with records in it. An application whose every list says No entries is an
application you cannot look at: nothing to sort, nothing to open, and every reference dropdown empty.
So the generator writes ten rows per entity into the model bundle and the runtime seeds them on first
boot. The values come from faker.js, but which value a column gets is decided by the
Application Dictionary rather than by faker — a column's reference type picks the generator, so an
EMAIL holds an address, a %%enum holds one of its declared values, and a
TABLE_DIRECT holds the id of a row that exists. Entities are written parents-first for
exactly that reason: a lookup that opens on a real record is the difference between sample data and
noise. Set it to None in step 2 if you would rather start from empty tables.
Your model never leaves your browser. It is parsed in this tab, the application is assembled in this
tab, the sample rows are invented in this tab, and the database it all fills lives in this tab's
IndexedDB — nothing you load here is uploaded anywhere, and every file this page needs is
served from this site.
One thing does leave: anonymous usage analytics, so we can see where readers get stuck — which
step ran, how long it took, how many errors the checker found. The model's contents are never
sent, and the recording of this page blanks out the editor, the model preview and the application
frame. What is collected, and how to turn it off →
Choose a model
Start from the CRM this guide is built around, or bring your own .mmd.
Look at the model
Generate the application
The same parser and compiler the appwithai-wasm command runs, bundled for the browser.
Two different applications, from the one model. Download as files writes out the
browser application above — the one running in the frame, no install and no build.
Download the deployable app assembles the other one: the real NestJS and TanStack Start
source the command-line generator writes, about four hundred files, with a
docker-compose.yml beside them. Unzip it and
docker compose up --build brings up PostgreSQL, the API and the web front end.
It is assembled here in your browser when you press the button, which takes a few seconds and
fetches the stack templates from this site.
Run it
Postgres starts, the dictionary and the sample records seed themselves, and the application comes up.
What actually happened
The generate step ran the same code the command-line tool runs — the Mermaid parser, the rule
compiler that turns %%rule flowcharts into GoRules JDM, the %%rbac compiler,
the workflow and hook compilers — and produced a set of files. Almost all of them are identical for
every model: the browser stack compiles a model into data (one model.json and one
schema file) read by a runtime that is the same bytes every time, rather than into a controller and a
service per entity. That is why generation finishes in milliseconds and why there is nothing to
install.
The run step handed those files to a Service Worker, which now serves them as if they had come
off a web server, and started a worker thread that opened Postgres, applied the schema, seeded the
Application Dictionary from your model, inserted the sample records and began answering
/api requests.
Those records were written at generate time, not at run time — they travel inside
model.json, and the runtime inserts them the first time it opens a database that has none.
It is worth saying what that costs and what it buys. It costs a larger model.json. It buys
a seed that ran through the schema your model actually describes: a row rejected by a
NOT NULL or a foreign key is a row you do not get, so an application that comes up full is
itself a statement about the model. Regenerating the same model gives the same records, because the
generator seeds faker from the application's name — a screenshot of row four stays a screenshot of
row four.
The dashboard of the running application opens on a Manual button, and what it points at was
written by the same generation run. manual.html is one self-contained page — a
contents menu, then a section per entity giving every field with the control it becomes, its
constraints, its enumerated values and its help text, followed by that entity's relationships, its
state machine, the rules that fire on it and the roles allowed to see it. It is derived from the
parsed model, the same source the schema and the guards come from, so it cannot describe an entity
that does not exist or miss one that does.
What it can miss is the prose, and that is the point worth taking away: the “what it is
for” column is your model's %%field <E>.<col> help: text and nothing
else. Where a model has none, the manual prints a dash and says so — a complete, accurate list
of fields that explains none of them. The CRM model this chapter loads carries help on every entity
and every column, which is what makes its manual readable, and is the argument for writing them as
you model rather than intending to later.
The same output can be written to disk and served anywhere static:
bun run wasm generate -i language/examples/crm.eml.mmd -o ./crm --name "Acme CRM" --vendor-pglite
bun run wasm serve ./crm
--vendor-pglite copies the WebAssembly build of Postgres into the output, so the
application needs no network at all. This site ships that build too, under
assets/vendor/pglite/, which is why the chapter loads nothing from a third-party host and
keeps working on a network that blocks them. It is about eighteen megabytes, fetched once and then
cached by your browser.
Where this stops
The roles in a generated application decide what it shows and refuses exactly as they would on a server, but the database here is a file in your own browser: they are not a secret from you. Nothing on this page is multi-user, nothing syncs, and closing the tab is the backup strategy.
The front end above is a port of the generated application's interface, not the same code. It is the same design system — the same typography, the same action bar, breadcrumb, category cards, grid and record panel — reading the same Application Dictionary through the same API. What it is not is the React and TanStack Router build, and two things prevent that being reused here rather than one: it has to go through Vite, and there is no bundler in a browser tab; and its session is a cookie, which a Service Worker never sees on a request it intercepts. So this one carries a token instead.
When the data needs to live somewhere other than the reader's machine — or when a developer needs a controller to open and edit — the same model goes through the full NestJS and TanStack Start pipeline, the one the rest of this guide is about, and comes out as an API and a front end that a team can deploy.