Development

July 22, 2026 · View on GitHub

What this is: how to build, run, and hack on Publisher from a clone. For deploying a built server (Docker, config, tuning), see deployment.md and configuration.md.

Prerequisites

ToolVersionRequired for
Bun≥ 1.3.13Primary runtime + package manager
Node.js≥ 20DuckDB postinstall scripts and the npx @malloy-publisher/server bin shebang
Python≥ 3.12Only if you build the Python client (packages/python-client)
Java≥ 21 (Corretto recommended)Only if you regenerate API clients via bun run generate-api-types

The repo ships a .tool-versions file compatible with mise and asdf, so mise install (or asdf install) provisions all four versions at once.

Sample packages are read from publisher.config.json, so no submodule checkout is needed. From a clone, that config points at the local examples/ directories, which are DuckDB-backed (storefront, governed-analytics, html-data-app) and need no cloud credentials. Nothing is fetched on first boot. (The published npx build has no repo to read, so its bundled default fetches the same packages from GitHub instead.) To enable the BigQuery-required samples, see configuration.md.

Editing the examples locally. Because the config points at examples/, the server already serves your working copy, not the committed versions on main. To have edits hot-reload, start in watch mode:

bun run build && bun run start -- --watch-env examples

(or set PUBLISHER_WATCH=examples). Watch mode mounts the packages in place as symlinks, which happens when the environment is first loaded: on a fresh server root, or on any boot with --init. If you already started the env without --watch-env, its packages were copied into publisher_data/ and edits to examples/ will not show up, so run once with both flags together, --watch-env examples --init, to re-mount them. The governed-analytics and html-data-app READMEs also have a self-contained "Run it standalone" recipe that mounts just that package from a /tmp workspace. After changing an example's data generator, re-run bun run generate:example-data to refresh the Parquet and CSV files.

Makefile shortcuts

A top-level Makefile wraps the common workflows so you don't have to remember script names or cd into individual packages. Run make help for the full list. The most useful targets:

TargetWhat it does
make installbun install at the repo root
make buildProduction build: SDK → app → server bundle
make start / make start-initRun the built server (--init clears persisted storage on boot)
make stopKill anything on ports :4000 or :4040
make devExpress + Vite together in one terminal with prefixed [server]/[react] logs (Ctrl+C kills both)
make dev-server / make dev-reactSame dev workflow, split into two terminals
make status / make environments / make packagesQuick API smoke checks
make test / make lint / make typecheck / make formatQuality gates
make regen-apiRegenerate server + SDK clients from api-doc.yaml (needs Java)

Production build

One command builds the SDK, app, and server bundle in order:

make install
make build
make start                # Run the built server (REST on :4000, MCP on :4040)

Or run the underlying bun scripts directly: bun install && bun run build && bun run start (bun run build:server-deploy is the same build with the app bundle produced by the server's build pipeline — make build uses it).

Dev mode

Express and Vite run as separate processes. Express on :4000 proxies non-API traffic to Vite on :5173 when NODE_ENV=development, so visit http://localhost:4000 for the full app — :5173 won't have API access.

One terminal (recommended):

make dev

This runs both servers with combined, color-prefixed logs ([server] / [react]). Ctrl+C stops both cleanly.

Two terminals (if you prefer split logs):

make dev-server          # Express (REST :4000 + MCP :4040, watch mode)
make dev-react           # Vite dev server (:5173, proxied through :4000)

Open http://localhost:4000.

Tests and quality gates

make test                # unit + integration server tests
make lint && make format # eslint + prettier
make typecheck           # tsc --noEmit across sdk/app/server

make typecheck (and the underlying bun run typecheck) depends on the SDK's emitted .d.ts files, which in turn depend on the OpenAPI codegen. On a fresh clone, build first — either with make build (full SDK + app + server bundle), or with the targeted minimum:

bun install
bun run generate-api-types
bun run build:sdk
bun run typecheck

After that, bun run typecheck works on its own as long as the SDK build artifacts stay current:

  • After editing api-doc.yaml → re-run bun run generate-api-types && bun run build:sdk.
  • After editing SDK source → re-run bun run build:sdk.

Contributing

See CONTRIBUTING.md for committers, the Developer Certificate of Origin sign-off, code-review process, and the Python SDK regeneration workflow.