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);Express
Section titled “Express”app.use('/crm', tw.node); // before express.json() or any other body parsertablewalk 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().
Next.js
Section titled “Next.js”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));Pages on other origins
Section titled “Pages on other origins”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.
Starting and stopping
Section titled “Starting and stopping”tw.readysettles once the App, sign-in and pools are loaded. Requests that arrive sooner wait for it; past ten seconds they are answered 503 withRetry-After. A startup that fails rejectsreadyand 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,policyandjobsname the files the command line’s--app,--commands,--policyand--jobswould. Withjobs, the App’s jobs run in your process and stop onclose(); without it, run them in atablewalk --jobsprocess of their own. A tenant App is mounted withappandpolicy, as it is started, and takes its embed origins from the config file.- A
basePaththat 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.
Related
Section titled “Related”- Build and deploy: a build mounts the same way;
name it with
app. - Several Apps in one server
- The general path: the handler beside a route of your own, and the App in your React app