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

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

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.

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:

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?