Quickstart: an App from scratch
Start a new App from an empty PostgreSQL database, a schema.sql and twenty-one lines of TypeScript.
You need Node 24 or newer and a PostgreSQL server you can create a database on. 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: tablewalk 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 tablewalk 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 tablewalk 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
Section titled “8. Serve it”npx tablewalk --app .You should now see Billing: http://localhost:4111/. Open it:
- 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.
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.
- 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.