Chapter 09

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 →

1

Choose a model

Start from the CRM this guide is built around, or bring your own .mmd.

2

Generate the application

The same parser and compiler the appwithai-wasm command runs, bundled for the browser.

Seeded, and shown on the sign-in screen.
The database is yours — there is nobody to keep it from.
Generated with faker.js and seeded on first boot.

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.

3

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:

shell
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.

Clear the databases from earlier runs?