Skip to content

Mount tablewalk in your server

Serve an App under a path of your own Node, Express, Next or Hono server, in its process, with no second port.

import { createTablewalkHandler } from 'tablewalk/handler';
const tw = createTablewalkHandler({
config: './tablewalk.json', // connections and "apps", as --config reads them
basePath: '/crm', // where your server hands requests over
publicOrigin: 'https://example.com', // the origin people's browsers use
});

Everything under /crm is the App: its pages, its API, its sign-in and its files. It is served exactly as npx tablewalk --config ./tablewalk.json --base-path /crm would serve it behind a proxy, so cookies carry Path=/crm, links in mail and the App’s addresses are said under /crm, and the Host and Origin checks expect publicOrigin.

import { createServer } from 'node:http';
createServer((req, res) => {
if (req.url?.startsWith('/crm')) return tw.node(req, res);
res.end('your own pages');
}).listen(3000);
app.use('/crm', tw.node); // before express.json() or any other body parser

tablewalk reads its own request bodies, so mount it ahead of a body parser; a request a parser has already read is refused with a message that says so. Mounted without a path (app.use(tw.node)), it passes anything outside basePath to next().

app/crm/[[...path]]/route.ts
import { createTablewalkHandler, type TablewalkHandler } from 'tablewalk/handler';
export const runtime = 'nodejs';
const shared = globalThis as { tablewalk?: TablewalkHandler };
// One handler per process: development reloads this module.
const tw = (shared.tablewalk ??= createTablewalkHandler({
config: './tablewalk.json',
basePath: '/crm',
publicOrigin: process.env.PUBLIC_ORIGIN!,
}));
const handle = (request: Request) => tw.fetch(request);
export { handle as GET, handle as HEAD, handle as POST, handle as PUT, handle as PATCH, handle as DELETE };
app.all('/crm/*', (c) => tw.fetch(c.req.raw));
createTablewalkHandler({ ...options, embed: { origins: ['https://shop.example.com'] } });

A page on a listed origin may call the App (CORS, answered for that origin alone) and frame it: /crm/?embed=frame lets the listed origins put the App in an iframe, where every other page is refused one. Cookies never cross origins: a listed page signs its person in with an embed assertion, and writes only with the token that gives it.

  • tw.ready settles once the App, sign-in and pools are loaded. Requests that arrive sooner wait for it; past ten seconds they are answered 503 with Retry-After. A startup that fails rejects ready and every request is answered 503 with the reason.
  • await tw.close() on shutdown stops the App’s jobs, waits for requests in flight and closes its pools and sign-in store.
  • app, commands, policy and jobs name the files the command line’s --app, --commands, --policy and --jobs would. With jobs, the App’s jobs run in your process and stop on close(); without it, run them in a tablewalk --jobs process of their own. A tenant App is mounted with app and policy, as it is started, and takes its embed origins from the config file.
  • A basePath that says one of tablewalk’s own paths (/api, /assets, /_tablewalk, /healthz, /readyz) is refused; under your API, as /api/tw, is fine.
  • A config that serves no App is refused: the database browser is not mountable, because its connection form trusts whoever reaches it.
  • Bun and Deno are not supported: the SQLite driver, the pools and the App loader are Node’s.