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: this page, like every page of the site, loads Google’s analytics tag, which
records page visits. It is handed nothing from the editor, the model or the application frame, and
there are no session recordings. What is collected, and how to turn it off →
That guarantee is about this chapter, and it still holds here. One page on this site does
send a model elsewhere, and only because that is what it is for: the
Model Assistant takes an API key of yours and asks a provider to
change a model for you. It is a separate page, it says so above its own key field, and nothing on
this chapter reaches it.
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 five hundred files, with a
docker-compose.yml beside them. Unzip it and
docker compose up --build brings up PostgreSQL, the API, the web front end
— and the Enterprise Reporting platform beside them, on its own port, holding
this model’s reports, charts and dashboard and one reporting role per role the model
declares. reporting/README.md in the archive names both sets of accounts.
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.
When two people save the same record
Every record carries a version that each save raises by one, and the form you edit a
record in remembers the version it opened. A save made after somebody else has already saved that
record is refused rather than applied, so the second person cannot overwrite the first without
seeing it. The form shows a panel listing the fields the other save changed — their value beside
yours, the fields you both changed marked — and offers Reload their version,
Overwrite with mine or Keep editing. It is a panel here rather than a dialog because the
application runs inside a frame on this page, where a dialog is not guaranteed to appear.
The deployable application in the .zip behaves the same way, with a dialog, and so does
its API to anything that is not a browser: a read answers with ETag: "<version>", a
save sends If-Match, and a stale one gets 409 with
VERSION_CONFLICT. The generated manual explains it to the people using the application.
The second application, and the second sign-in
The dashboard carries an Enterprise Reporting section, and clicking through it does not open a screen of the application you are signed into — it asks you to sign in again. That is the whole point of it. One model generates two applications, and this is the other one: the reporting platform, holding the reports, charts and dashboard derived from the same document, over the same data the application above has been writing.
Two logins, and they are not merged. A role in the application decides what you may do
to a record. A role here decides which of the application's tables your queries may read — and
those are different questions, answered by different tables in different sign-in screens. The names
line up, deliberately: the CRM model declares seven functional roles and the reporting side has one
account per role, at support.agent@crm.reports.example.com against the application's
support.agent@crm.example.com. The addresses differ on purpose, because identical ones
would invite you to try one password on both, and neither password works on the other side.
Signing in as support.agent@… is the thing to try. It reads five of the model's
seventeen tables, so it is offered 36 of the 116 reports and its dashboard says
7 tiles hidden — Support Agent may not read the tables behind them. Ask it for a report over
an entity it does not own and it refuses by name. The administrator reads all seventeen. None of that
is configured here: it is the same %%rbac … .read lines the application's navigation is
scoped from, read a second way.
Sign in as the administrator and the sidebar grows an Administration section, and it works: Users adds, edits and deactivates reporting accounts, Roles creates them, and Permissions decides which of the application's tables each role may read — take one away from the Support Agent and its next report over that table is refused, not its next session. Data Sources shows the application's database with live row counts, and System Logs records every sign-in, every run and every refusal, and every change made under Administration.
This is a preview of the platform, and it says so on every screen. Deployed, Enterprise
Reporting is a separate product — enterprise_reporting_tanstack, a server over
PostgreSQL — and the Download the deployable app archive builds it from its own source,
unmodified, on its own port beside the application. A browser tab cannot run that server, so here
the same reports, roles and accounts are served from this tab and drawn in the platform's own
layout: its sidebar, its header, its cards and tables, its colours. What needs the platform's
servers — the SQL editor, natural-language queries, jobs, monitoring, the report generator,
the trigger board and settings — stays in the sidebar where the platform puts it, and opens a
page naming where the real one is.
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.