Building

Primate runs on Node, Bun, and Deno via rcompat. It avoids runtime-specific APIs and abstracts differences via rcompat. You build and run with your chosen runtime, and target Web or Native.

Why rcompat

Using rcompat keeps apps portable and future-proof:

Build and run

Dev

# Node
npx primate

# Bun
bunx --bun primate

# Deno
deno run -A npm:primate

Build

# Node
npx primate build

# Bun
bunx --bun primate build

# Deno
deno run -A npm:primate build

The command exits nonzero when compilation, migration artifact generation, or another build step fails, so CI and deployment scripts can stop immediately. It also evaluates route declarations once in an isolated, time-limited process to catch errors such as a body schema without contentType. This does not start the application server or invoke route handlers, serve hooks, or migrations. Route modules and their imports are trusted application code: top-level initialization can perform I/O and must work on the build host, including when building for a different runtime. The validator uses the build host's runtime conditions; target-specific declarations still need testing on their target runtime.

Serve a Build

# Node
npx primate serve

# Bun
bunx --bun primate serve

# Deno
deno run -A npm:primate serve

Update guidance

Run primate update from an application to send its installed release line to the update service and print the todos for the single immediate next stable release, when one exists:

primate update

The command is read-only. It does not install packages, edit application files, run migrations or execute returned todos. It never skips release lines; after manually completing one update, run it again to request the next transition. Unknown release lines and service failures are reported as errors rather than being interpreted as proof that the application is current.

Unified APIs with rcompat

Prefer rcompat packages over runtime-specific APIs.

Filesystem

import fs from "@rcompat/fs";

const base = fs.ref("static");
const file = base.join("logo.svg");

HTTP Status

import http from "@rcompat/http";

return new Response("ok", { status: http.Status.OK });

MIME Types

import http from "@rcompat/http";

const known = http.MIME.TEXT_PLAIN;  // "text/plain"
const byExt = http.toMIME(".svg");  // "image/svg+xml"

Runtime Support

Primate supports multiple runtimes and build targets. A blank cell below is not a claim of support; check the adapter's documentation and test on your chosen runtime and target before deploying.

Build Targets

Target Node Deno Bun
Web
Desktop (linux-x64)
Desktop (darwin-x64)
Desktop (darwin-arm64)
Desktop (windows-x64)

Backends

Backend Node Deno Bun (web) Bun (native)
Go
Python
Ruby
TypeScript

Databases

Database Node Deno Bun (web) Bun (native)
MemoryDB
JSONDB
MongoDB
MySQL
Oracle
PostgreSQL
SQLite

Web target

The Web target serves your app over HTTP. It's the default target and runs on all runtimes (Node, Bun, Deno). primate build creates the server bundle and assets; primate serve runs the built app.

Build output

build/
└── server.js         # Production server entry

Serving

npx primate serve
# or
node build/server.js

Native target

The Native target compiles your Primate application into standalone desktop executables for Windows, macOS, and Linux. It embeds your app and uses a system WebView to render the UI.

Under the hood, Primate runs an HTTP server inside the executable and points the WebView at it, so you ship a single desktop app without needing a separate web server.

Setup

Install the native module:

npm install @primate/native

Platform prerequisites

Linux x64 uses the WebKitGTK 6.0 sidecar from @rcompat/webview. Running a native app locally, in CI, or in a container requires GTK 4, WebKitGTK 6.0, and libsoup 3 from the host distribution:

# Arch Linux
sudo pacman -S webkitgtk-6.0

# Debian and Ubuntu
sudo apt install libgtk-4-1 libwebkitgtk-6.0-4

# Fedora
sudo dnf install gtk4 webkitgtk6.0

For a Debian or Ubuntu container, install libgtk-4-1 and libwebkitgtk-6.0-4 in the image before launching the executable. Building or cross-compiling alone does not load these libraries, but a launch smoke test does.

GTK 3, WebKitGTK 4.0, and libsoup 2 are not ABI-compatible substitutes. Do not work around a missing library with a compatibility symlink.

macOS uses the system WebKit framework. Windows uses the system WebView2 runtime; the Linux packages above are not required on either platform.

Configure your application:

// config/app.ts
import config from "primate/config";
import native from "@primate/native";

export default config({
  modules: [native()],
});

How it works

Primate compiles a launcher into the executable: the parent process serves your built app and a child process owns the WebView window, whose event loop must run on the process main thread (a requirement of macOS AppKit). Navigation, cookies, and fetch requests work as they do in the browser.

The build embeds your bundled server into the executable -- routes, middleware, modules, client bundles, and static files alike -- so the compiled app runs on its own, without the build directory it was produced in. The launcher bakes the start URL into the executable at build time from your http config, with 0.0.0.0 and empty hosts rewritten to 127.0.0.1 (WebKit refuses those spellings). At launch, an explicitly set PORT environment variable wins and the window navigates the resulting URL; otherwise the server is pinned to the baked-in port, so the compiled app always serves and navigates the same URL. The child probes the server on both localhost and 127.0.0.1 spellings before navigating. If the server never becomes reachable, or the webview fails to load, the executable exits with a nonzero code instead of starting a broken window.

Native builds compile with Bun only. The launcher relies on Bun's compiled executable layout and re-execs itself with an exact argv; every other runtime is rejected at build time. Run native builds with bunx --bun primate.

If you proxy to external services, keep in mind you're now a desktop process. Network access, TLS, and CORS behave like any other HTTP client.

Building

Development

Run the dev loop with a Webview window attached to the watch server:

bunx --bun primate dev --target=desktop

No binary is compiled: the window is an uncompiled script navigating the watch server, and on every file change, primate rebuilds the server bundle and reloads the page in the window. Close the window to leave the dev loop, and Ctrl-C it to stop the server. Window requests appear in the terminal prefixed with [native].

Production

Build a native executable for the host platform:

bunx --bun primate build --target desktop

This will create an executable app (or app.exe on Windows) inside build. The embedded server makes the executable self-contained: copying it out of build and running it is enough to launch the app.

Cross-compilation

You can target specific platforms from your development machine:

{
  "scripts": {
    "build:linux-x64": "bunx --bun primate build --target linux-x64",
    "build:darwin-x64": "bunx --bun primate build --target darwin-x64",
    "build:darwin-arm64": "bunx --bun primate build --target darwin-arm64",
    "build:windows-x64": "bunx --bun primate build --target windows-x64"
  }
}

Run, for example:

bunx --bun primate build --target windows-x64

Build Output

build/
└── app              # Linux/MacOS/Windows executable (app.exe on Windows)

The executable embeds your bundled server and all client and static assets: it is the only thing you need to copy when distributing the app, plus any sidecar directories modules have declared (see Sidecars below).

Configuration

Option Type Default Description
start string "/" Initial path when app launches
debug boolean false Enable debug logging
width number 1280 Initial window width
height number 720 Initial window height

Example

import native from "@primate/native";

export default config({
  modules: [
    native({
      // start URL when app launches
      start: "/",

      // enable debug mode
      debug: false,

      // window size
      width: 1280,
      height: 720,
    }),
  ],
});

Sidecars

Modules of a Primate app can contribute runtime files that a compiled app needs next to its executable. A module declares these files as sidecars through Primate's build contract; the Native module copies each declared file into the build directory, with its declared relative path preserved -- so the compiled app resolves it from its own directory at launch.

Sidecars are declared by the module that owns the runtime in question; Native itself has no knowledge of which modules or runtimes an app was built with. Apps without declarations build executables without sidecars. The most common use is the Pyodide runtime of @primate/python apps; see the Python documentation for details.

Sidecar files are validated at declaration: destinations have to be portable relative paths inside the build output, and a declared file that is missing or uncopyable fails the build instead of shipping a broken distribution.

Distribution

The built executable is the distributable: it embeds the bundled server and all client and static assets, so it runs from any location you copy it to. Sidecar directories modules declared (see Sidecars above) ship alongside it; the most common is the Pyodide runtime directory of @primate/python apps. Shipping native apps usually involves platform packaging and signing:

These steps depend on your release tooling; integrate them after primate build completes.

Previous
Modules
Next
Testing