Skip to content

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.

In an empty folder:

Terminal window
npm init -y && npm pkg set type=module && npm install tablewalk typescript

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

Terminal window
createdb billing

You should now see nothing: createdb is quiet when it works.

schema.sql
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”
tablewalk.json
{
"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.

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

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

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

Terminal window
npx mistri dev

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

Name the invoices that are still owed once, on the resource, and give the list a tab for each:

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

In a second terminal, run what CI would:

Terminal window
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).

Terminal window
npx mistri build --app . --config tablewalk.json

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

  • 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 the url.

  • On SQLite, name a file in the url ("./billing.db") and skip createdb; the same schema.sql serves, its serial primary key read as SQLite’s integer primary key.

  • mistri dev refuses to start → NODE_ENV=production is set in the shell; dev is for this machine only. Serve with npx mistri --app . instead.