Skip to content

A few lines of TypeScript.
A complete business App.

Describe your records, screens and rules. tablewalk serves the App on PostgreSQL, with lists, record pages, forms, dashboards, sign-in and roles, and an assistant. Write it yourself, or have your coding agent do it.

lintel/resources.ts
machine: machine('status', {
draft: { on: { signLease: 'active' } },
active: { on: { giveNotice: 'notice' } },
notice: { on: { endLease: 'ended' } },
ended: { final: true },
}),
lintel/pages/lease.ts
page('lease', [
header({ status: 'status',
facts: ['start_on', 'end_on', 'rent', 'deposit'] }),
fields(['balance', 'charged', 'paid']),
related('Charges', 'charge', 'lease_id', /* … */),
// … payments
card('Home', 'unit_id', ['property_id', 'bedrooms', 'sqft']),
card('Resident', 'resident_id', ['email', 'phone']),
]),
A Lintel lease, Alder Court #101, Active: its start, end, rent and deposit under the name, Give notice and Add charge, the road from Draft to Ended, the balance due beside what was charged and paid, its charges, and beside them the home and the resident.A Lintel lease, Alder Court #101, Active: its start, end, rent and deposit under the name, Give notice and Add charge, the road from Draft to Ended, the balance due beside what was charged and paid, its charges, and beside them the home and the resident.

Two tables and twenty-one lines of TypeScript are an App you can list, search, filter and edit. Write the schema you want and the screens over it.

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
);
tablewalk.json
{
"connections": [{
"name": "billing",
"url": "postgres://localhost/billing",
"writable": true
}]
}
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',
});
Terminal
npm init -y && npm pkg set type=module && npm install tablewalk typescript
createdb billing
npx tablewalk migrate diff init --app . # writes migrations/V1__init.sql from schema.sql
npx tablewalk migrate up --app . # applies it, then checks the App against it
npx tablewalk --app .

The first migration is written for you to read before it runs, and the next comes the same way when you change schema.sql. No front end to write, no API to wire, no build step to serve. Quickstart: an App from scratch

Already have a database? tablewalk drafts the App from it. Quickstart: an App from your database

Lintel, the flagship example, runs a property company’s three Denver buildings: leases, a rent ledger, repairs, a storeroom, a public listings site and a resident portal. It is about 1,500 lines of TypeScript, commands and jobs included, not counting blank lines, comments or tests, checked against the database before anyone opens a page. Three cuts from it:

A list people work in

examples/apps/lintel/views.ts
units: listView('Rent roll', 'unit', {
icon: 'buildings',
// …
editable: ['market_rent', 'available_on', 'listed'],
// …
tabs: [

An asking rent, a date and a listing change in place; the tabs are the rent roll’s segments, each counted.

Lintel's rent roll: counted tabs from All to Advertised, filters to add, and rows of homes with their size, asking rent, status and resident.

Buttons from the road a lease travels

examples/apps/lintel/resources.ts
machine: machine('status', {
draft: { on: { signLease: 'active' } },
// …
ended: { final: true },

Each state names the verbs that leave it. They become the page’s buttons and its stepper, and the server refuses any other change of status.

A Lintel lease that is Active: Give notice and Add charge, and a strip of its states from Draft to Ended.

A card for each number that matters

examples/apps/lintel/dashboards.ts
metric('Rent collected', 'charge', {
query: 'kind = rent and status = paid',
over: 'due_on',
measure: 'sum amount as collected',
better: 'up',
// …
}),

Over the window the reader picks, with its change from the period before and its trend across the window, drawn unasked.

Lintel's figures over the last 90 days: occupied and vacant homes, Rent collected with its change against the prior 90 days and a sparkline, and Overdue.

Every list, record page, form and dashboard is drawn for you, in a design system made for work people do all day.

The assistant in a CRM test App: a bar chart of the open pipeline by stage that it drew from the data, and below it a proposed change, Start working Bronwyn Quinn, with Confirm and Decline buttons.
Ask in words. The assistant answers with the App’s own charts, tables and record cards, as the person signed in, and changes nothing until they confirm. One assistant() entry. How it works
Lintel’s repair board: open repairs as cards in a lane per state, New, Triaged, Scheduled and In progress, each card with its kind, home, technician and date.
A board with a lane per state, or the same rows as a grid with counted tabs and filters, a calendar or cards. Each is one listView call. How it works
A repair in Lintel, beside the list it was picked from: its state, home, priority, technician and visit, a Schedule visit button, and its conversation with the resident and an internal note.
A record page: the buttons its state allows, its fields, its conversation and its related lists. Sections in one page(). How it works
Lintel’s Today: the window button, four figures with Rent collected’s change and trend, the repairs counted by state, rent charged and collected by week as bars, and homes by building.
Metrics, counts by state, bars and lists over your own queries, in a window the reader picks. A metric or chart call each. How it works
  • Keys for everything. ⌘K finds any view, and j and k step through the records of the list you came from. Navigation
  • An address for every state. A list’s tab, search, filters and sort, a record, a dashboard’s window: copy the address and share it. Addresses
  • Edit in place. Name the columns a list may change in its cells, and a record’s side panel edits where it reads. Lists
  • Forms from the schema. Required fields marked, references searched by name, a switch for a yes or no. Forms
  • Phone layouts. On a phone the menu folds into the bar, dialogs fill the screen and a record’s side panel follows the page. Navigation
  • Light and dark, three looks. Lumen, Mono and Sage; each reader picks a look and System, Light or Dark under Preferences. Looks
Lintel's Today dashboard twice, side by side. On the left in Lumen, the default look: white panels, rounded corners and four figure cards in a row. On the right in Blueprint, a theme of its own: blue ink on a gridded off-white ground, square corners, the greeting in condensed capitals, and the figures and their labels in a monospace under ruled lines.
Lintel's Today in Lumen, the default look: white panels, rounded corners and four figure cards in a row.
The same page in Blueprint: blue ink on a gridded off-white ground, square corners, the greeting in condensed capitals, and the figures and their labels in a monospace under ruled lines.

The same Lintel, data and addresses, re-dressed by one validated 61-line theme file that changes 98 of Lumen’s values. tablewalk check tests every color pair it makes in light and dark, and refuses one it cannot read, naming a color that passes; tablewalk grade then measures the rendered pages, 26 of them at four widths: 99 of 100, and no finding the theme causes. A look of your own

When a default is not enough, each layer has a hook, and what you write is still checked against the database.

A component of your own

export const warehouseBins = custom('Floor', binMap, {
rows: {
bins: { from: 'bin', whose: 'warehouse_id', /* … */ },
},
});

A floor map drawn in React with tablewalk/ui: typed rows in, read under the reader’s grants, changes only through commands, in every look.

Depot's warehouse page: the Floor section, a bin map drawing each aisle's bays and levels shaded by how full each bin is.

tablewalk is made for a coding agent to build with: little to write, a precise answer when it is wrong, and a way to see what the running App did. Install the skill, give the agent a brief, and it works against the same checks you would.

  • The skill where your agent reads it. tablewalk agent init installs it for Claude Code, Codex, Cursor and GitHub Copilot. Build with a coding agent
  • The schema in its context. tablewalk schema --brief prints one line per table, with keys, references and allowed values.
  • Wrong fails before a page opens. tablewalk check typechecks the App against the generated schema types and validates it against the database: a stable code, the file, and the fix, as lines or JSON. Check codes
  • It sees what it did. --dev records every request’s SQL, each command’s input and refusal cause, logs, N+1 and slow statements and page vitals, and tablewalk observe --mcp hands them to the agent as tools; tablewalk shot draws the App’s pages as PNG files. Observe
Terminal
$ npx tablewalk agent init
$ npx tablewalk schema --brief
…
customer(id* int, name text, email text?)
invoice(id* int, customer_id int → customer, total num, paid bool)
$ npx tablewalk check --app . --brief
unknown-column app.ts view "invoices": This view's show cannot be resolved on "invoice": "amount" ("amount" is not a column on invoice.). — fix: use a column the table has (the message names the nearest; tablewalk schema --brief lists them)

To read the data itself, tablewalk --mcp gives an agent up to twenty-three tools: seventeen that only read, and six writes listed only for a writable connection. In production the same traces go to your collector over OpenTelemetry. MCP · Observe and operate

What is stable and what comes next: Status and roadmap.

Three sample Apps to run and read: Tick, a checklist in one file; Lintel, property operations in modules with its own tests; and Ladle, a cooking school’s own React site with the staff App embedded in it. A CRM, a service desk, a warehouse and the like are patterns in the agent skill’s recipes, not samples. Sample Apps

Also a database browser. npx tablewalk postgres://… opens any PostgreSQL, MySQL/MariaDB or SQLite database read-only, and you follow its foreign keys like links. Apps run on PostgreSQL and SQLite; MySQL/MariaDB is for browsing. Browse a database

MIT license (the LICENSE file in the npm package)