Quickstart: an App from a schema
Start an App from a schema you write, on an empty PostgreSQL database, with twenty-one lines of TypeScript; then serve it, change it, check it and build it.
You need Node 24 or newer and a PostgreSQL server you can create a
database on. Nothing to start from yet? npx mistri new
makes a starter App. Already have a database? Draft the App from it.
1. Make the project
Section titled “1. Make the project”In an empty folder:
npm init -y && npm pkg set type=module && npm install tablewalk typescriptYou should now see added … packages and found 0 vulnerabilities.
TypeScript is the project’s own: mistri check types the App with it, and
with no other.
2. Make the database
Section titled “2. Make the database”createdb billingYou should now see nothing: createdb is quiet when it works.
3. Say what the tables are
Section titled “3. Say what the tables are”create table customer ( id serial primary key, name text not null, email text);
create table invoice ( id serial primary key, customer_id integer not null references customer (id), total numeric(10, 2) not null, paid boolean not null default false);Write a reference in full, references customer (id): the diff tool below
reads a bare references customer wrongly on SQLite.
4. Name the database, and let the App write
Section titled “4. Name the database, and let the App write”{ "connections": [{ "name": "billing", "url": "postgres://localhost/billing", "writable": true }]}Writing is opt-in for each connection: without "writable": true the App
only reads, whatever the App says.
5. Write the App
Section titled “5. Write the App”import { defineApp, listView } from 'tablewalk/app';
export default defineApp({ id: 'billing', name: 'Billing', connection: 'billing', resources: { customer: { access: 'write' }, invoice: { access: 'write' }, }, views: { invoices: listView('Invoices', 'invoice', { show: ['customer_id', 'total', 'paid'], }), customers: listView('Customers', 'customer', { show: ['name', 'email'], }), }, nav: ['invoices', 'customers'], home: 'invoices',});You should now see app.ts, package.json, schema.sql and
tablewalk.json beside node_modules.
6. Write the first migration
Section titled “6. Write the first migration”npx mistri migrate diff init --app .You should now see wrote migrations/V1__init.sql: and the SQL it holds:
the first migration is schema.sql itself, from create table customer to
references customer (id), with nothing downloaded. Read it before it runs. The next migrate diff,
after you change schema.sql, fetches the diff tool once (Atlas, about 40 MB)
into .tablewalk/bin; add .tablewalk/ to .gitignore.
7. Apply it
Section titled “7. Apply it”npx mistri migrate up --app .You should now see applied V1__init.sql, Applied 1 migration: now at version 1., and then the App checked against the tables it made:
typecheck passed and validation passed Billing: ok.
8. Serve it while you work
Section titled “8. Serve it while you work”npx mistri devYou should now see Billing: http://localhost:4111/, and beside it the dev
observer’s address (every request’s SQL, refusals and timings). Open the App:
- Invoices, empty (
No rows match.), with New invoice; Customers beside it in the menu; - the New invoice form asks for a Customer and a Total, both required, and sets Paid with a switch that starts at No;
- add a customer and its page opens at
/customers/1, with its invoices listed under it.
mistri dev is for this machine only: it keeps a throwaway session secret, and
a sign-in for each role once the App has them, in .tablewalk/. Add it to
.gitignore. A save reloads the App; leave the server running.
9. Make one change
Section titled “9. Make one change”Name the invoices that are still owed once, on the resource, and give the list a tab for each:
import { defineApp, listView } from 'tablewalk/app';
export default defineApp({ id: 'billing', name: 'Billing', connection: 'billing', resources: { customer: { access: 'write' }, invoice: { access: 'write', segments: { unpaid: 'paid = false', settled: 'paid = true' }, }, }, views: { invoices: listView('Invoices', 'invoice', { show: ['customer_id', 'total', 'paid'], tabs: ['unpaid', 'settled'], }), customers: listView('Customers', 'customer', { show: ['name', 'email'], }), }, nav: ['invoices', 'customers'], home: 'invoices',});You should now see, after the save, Unpaid and Settled tabs above the
invoices, each with its count. A segment is a condition in the
query language, and the tab’s address (?tab=settled) opens it
again.
10. Check it
Section titled “10. Check it”In a second terminal, run what CI would:
npx mistri check --app .You should now see typecheck passed and validation passed, and an exit
code of 0. Misspell a column ('piad = false') or a tab (tabs: ['upaid']) and run it
again: the problem is named with its file, its line and the fix
(mistri check).
11. Build it for production
Section titled “11. Build it for production”npx mistri build --app . --config tablewalk.jsonYou should now see a line per step (typecheck, validate, bundle, database)
and built …/billing-build, with the command that serves it. A build is
optional (the source runs as it is), but it checks once in CI and starts
faster. Build and deploy takes it into a container,
behind a load balancer, with its migrations run first.
If it fails
Section titled “If it fails”-
App error: … "Billing" declares the tables "customer" and "invoice", which billing does not have→ the migration is not applied yet: run step 7. -
createdb: error: connection to server … failed→ PostgreSQL is not running, or not on its default port; start it, or put the host, port and user in theurl. -
On SQLite, name a file in the
url("./billing.db") and skipcreatedb; the sameschema.sqlserves, itsserial primary keyread as SQLite’sinteger primary key. -
mistri devrefuses to start →NODE_ENV=productionis set in the shell;devis for this machine only. Serve withnpx mistri --app .instead.
- Tick in fifteen minutes: add a tab, an action and a look to a running App.
- Change the schema from schema.sql: edit
schema.sql, and the nextmigrate diffwrites the migration. - Build with a coding agent: hand the next screens to an agent, with a loop and a rubric.
- Tenant Apps: one App for many
customers needs text ids (
id text not null primary key), or keys and unique indexes that include the ownership column;serialids need the second.