Primate LogoPrimate

Configuration

Primate works out of the box with zero configuration. In some cases, you may wish to change the defaults. The most common use case is activating additional modules.

Configuration files are located in config. Anything you configure is merged into the defaults.

app.ts

TypeScript JavaScriptconfig/app.tsconfig/app.js
import config from "primate/config";

export default config({
  /* options */
});

config/app.ts exports the application facade for your app. For using that value at runtime — including app.config(), app.env(), app.view(), and app.root — see the Application page.

App options

Option Default Description
build.external [] packages excluded from the server bundle
client.partitions undefined isolated client bundle partitions
client.routes undefined route-prefix to bundle-partition mapping
conditions [] additional package export conditions
migrations undefined application migration ledger configuration
entrypoints {} named client entrypoints
http.csp {} content security policy
http.headers {} default HTTP response headers
http.host "localhost" server host
http.port 6161 server port
http.timeout undefined HTTP request and inactivity deadlines
http.ssl.cert undefined path to SSL certificate
http.ssl.key undefined path to SSL private key
http.static.root "/" web path of static assets
loaders {} esbuild file loaders by extension
env.schema undefined schema for typed environment variables
modules [] extension modules
i18n.defaultLocale undefined default locale
i18n.locales [] list of locales
i18n.currency "USD" active currency
i18n.persist "cookie" locale persistence mode

build.external

Packages and import paths to exclude from the production server bundle. At runtime they are loaded from node_modules instead. Use this for dependencies that load adjacent WebAssembly modules, native addons, or data files that would otherwise be separated from the package by bundling.

import config from "primate/config";

export default config({
  build: {
    external: ["satori", "@resvg/resvg-js"],
  },
});

External dependencies must be installed wherever the production server runs. Development already keeps packages external by default.

conditions

Additional package export conditions used by both client and server builds. For example, a monorepo can resolve workspace packages directly from their TypeScript sources:

import config from "primate/config";

export default config({
  conditions: ["@primate/source"],
});

Packages that do not expose a matching condition continue to use their normal exports.

migrations

Configuration for Primate's opt-in application migration system. db persists the execution ledger; migration bodies may perform other one-time application transitions as well as database schema changes.

import config from "primate/config";
import db from "@/config/db";

export default config({
  migrations: {
    table: "migration",
    db,
  },
});

Apply migrations explicitly with primate migration apply during development or build/migrate.js apply before starting a production server. Automatic startup application is not supported. See the Stores page for the complete migration graph workflow and 0.42 upgrade procedure.

client.partitions

Client partitions isolate browser code and assets by URL route prefix. Each partition is an independent build branch, compiled into its own output file tree. Shared dependencies are duplicated rather than emitted as chunks shared across an access boundary.

import config from "primate/config";

export default config({
  client: {
    partitions: {
      public: "/",
      admin: "/admin/app/",
    },
    routes: {
      "/": "public",
      "/admin": "admin",
    },
  },
});

client.partitions maps partition names to asset URL prefixes. client.routes maps URL path prefixes to those names; values that are not keys in client.partitions are rejected. The longest matching route prefix wins, and prefixes match whole path segments: /admin includes /admin/users but not /administrator. A / assignment is required as the default and is checked during startup. Mappings to unknown partitions, duplicate normalized mappings, and duplicate asset prefixes are also rejected during startup.

In 0.42, client.partitions replaces the 0.41 client.groups key. Rename the key when upgrading; the old key is rejected with a migration diagnostic rather than treated as a virtual route group. client.routes is unchanged.

Each partition receives its own bootstrap, route-page registry, CSS, dynamic imports, and file-loader assets beneath its configured asset prefix. Top-level files in views are shared deliberately and are duplicated into every partition; put partition-private pages and layouts beside their handlers in routes.

Imported fonts, images, and other file-loader assets are likewise emitted under each partition's prefix, even when their contents are identical. Distinct URLs are part of the isolation guarantee. Assets that are deliberately public and shared across every authorization boundary can instead live in static.

Navigation within a partition keeps normal client routing. Navigation across partitions stays in the same document: Primate first loads the destination assets and inactive styles and waits for its frontend to become ready. It then disposes the active frontend instance, replaces its partition styles, and mounts the destination. Links, form redirects, and browser history all use this transition. If a destination asset cannot be loaded or is rejected by the proxy, the active app, URL, and styles remain in place. Static module code may remain in the browser's module cache after disposal.

This allows a reverse proxy to apply the same authentication rule to both the HTML route and its complete client file tree. For example, protect /admin and /admin/app/, with the more specific asset rule evaluated before a catch-all public rule. Production filenames are content-hashed and can be cached as immutable assets; HTML should not be cached with the same policy.

Client partitions provide asset isolation, not application authorization. Continue to authorize every protected request in route hooks or handlers. Source maps are not emitted by Primate's client production build.

entrypoints

Named client entrypoints bundled from the client directory and injected into templates/app.html as placeholders.

import config from "primate/config";

export default config({
  entrypoints: {
    css: "master.css",
    colorscheme: "colorscheme.ts",
  },
});

Each key becomes a placeholder in your HTML template. For a key css, add %css% wherever you want the asset injected:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    %css% %head%
  </head>
  <body>
    %body%
  </body>
</html>

Entrypoint keys follow the placeholder naming rules: they start with a letter or underscore and may contain letters, numbers, underscores, and hyphens.

Any file type supported by your active frontend modules works as an entrypoint — if you have @primate/svelte configured, you can point an entrypoint at a .svelte file and it will be bundled and injected like any other asset.

The names app, head, and body are reserved and cannot be used as entrypoint keys.

http.csp

The Content Security Policy (CSP) to use.

Example of a restrictive policy.

{
// all content must come from own origin, excluding subdomains
"default-src": ["'self'"],
// styles must come from own origin, excluding subdomains
"style-src": ["'self'"],
// disallow <object>, <embed> and <applet> elements
"object-src": ["'none'"],
// disallow embedding
"frame-ancestors": ["'none'"],
// all form submissions must be to own origin
"form-action": ["'self'"],
// allow only own origin in <base>
"base-uri": ["'self'"],
}

http.headers

HTTP headers to use when generating requests using the view handler.

http.host

The HTTP host to use. This value is directly passed to the runtime.

http.port

The HTTP port to use. This value is directly passed to the runtime.

http.timeout

An optional strict object containing separate network clocks, all expressed in whole milliseconds:

import config from "primate/config";

export default config({
  http: {
    timeout: {
      inactivity: 30_000,
      request: 30 * 60_000,
      headers: 60_000,
    },
  },
});

Each member is optional. Omitting a member preserves the runtime default. 0 disables a supported clock. inactivity accepts 0 through 255000; request and headers accept 0 through 2147483647. If nonzero headers and request values are both configured on Node, headers cannot exceed request. An explicit Node header timeout is also bounded by Node's effective request timeout when request is omitted. Header receipt is subject to both headers and request, and the first expiration wins. Setting Node's request to 0 also makes an omitted headers clock default to 0.

Runtime inactivity request headers Defaults when omitted
Node supported supported supported disabled; 300 s; lesser of 60 s or request
Bun supported, rounded up to seconds unsupported unsupported 10 s inactivity
Deno unsupported unsupported unsupported native Deno.serve behavior

Configuring an unsupported member, including 0, fails during server startup; Primate never silently substitutes a handler deadline. During development, Primate disables inactivity on Node and Bun so debugging and live reload are not interrupted. Configured request/header receipt clocks remain active. Deno receives no synthetic development timeout.

Node checks incomplete requests on its native cadence, currently 30 seconds, so an expired request or headers clock may be observed after its configured threshold. Reverse proxies and load balancers impose independent deadlines; the shortest applicable timeout in the complete request path wins. Increasing or disabling receive deadlines weakens protection against slow-client attacks.

For server-sent events and other long-lived responses, either disable inactivity or send heartbeats more frequently than the shortest inactivity timeout in the path. For large uploads over slow connections, raise request without confusing it with a route-handler execution deadline.

The numeric 0.42 form is rejected with a migration diagnostic. Wrap the old value as inactivity:

// 0.42
http: { timeout: 30_000 }

// 0.43
http: { timeout: { inactivity: 30_000 } }

http.ssl.cert

Path to SSL certificate. If this property and http.ssl.key are set and point to a valid key/certificate pair, Primate uses https instead of http.

http.ssl.key

Path to SSL key. If http.ssl.cert and this property are set and point to a valid key/certificate pair, Primate uses https instead of http.

http.static.root

The path at which to serve static assets (those located in the static directory). Static assets take precedence over routes. This option allows you to have all static assets served at a subpath, like /public.

loaders

A map of file extensions to esbuild loaders. Currently only "file" is supported, which copies the asset into the build output and replaces the import with its URL — useful for fonts and other binary assets referenced from CSS.

import config from "primate/config";

export default config({
  loaders: {
    ".woff2": "file",
  },
});

env.schema

A Pema object schema used to validate and type environment variables exposed through the application facade.

import config from "primate/config";
import p from "pema";

export default config({
  env: {
    schema: p({
      API_TOKEN: p.string,
      PORT: p.u16,
    }),
  },
});

With env.schema configured, app.env(key) validates values when the app starts serving and becomes type-aware. See Pema objects for object schemas and the Application page for usage.

modules

Additional modules to load at runtime.

i18n.defaultLocale

The default locale to use, must be one from the locales list.

i18n.locales

List of locales to use, must have at least one locale.

i18n.currency

Currency to use in localization.

i18n.persist

Locale persistence mode.

Reference

import type { Module } from "@primate/core";
import type { FileRef } from "@rcompat/fs";

interface Config {
  http?: {
    csp?: Record<string, string>;
    headers?: Record<string, string>;
    host?: string; // "localhost"
    port?: number; // 6161
    ssl?: {
      cert?: FileRef | string;
      key?: FileRef | string;
    };
    static?: {
      root?: string; // "/"
    };
  };
  modules?: Module[];
}

session.ts

TypeScript JavaScriptconfig/session.tsconfig/session.js
import session from "primate/session";

export default session/*<SessionShape>*/({
  /* options */
});

Session options

Option Default Description
cookie.httpOnly true mark cookie as HttpOnly
cookie.maxAge none persist cookie for this many seconds
cookie.name "session_id" name of the session cookie
cookie.path "/" path for which the cookie is valid
cookie.sameSite "Lax" SameSite cookie policy
cookie.secure app TLS setting mark cookie as Secure
store default in-memory store store managing sessions

Whether the session cookie should be marked HttpOnly and hidden from client-side JavaScript.

The number of seconds for which the browser should retain the session cookie. When omitted, it is removed when the browser session ends.

The name of the session cookie.

The session cookie path (paths on which the cookie is loaded).

The level of security to use in sending the session cookies when browsing between websites.

Whether the session cookie should be marked Secure. By default, this follows the app's TLS setting. Set it explicitly when TLS terminates at a trusted reverse proxy.

store

The store used to validate and persist sessions. By default, Primate uses an in-memory store that accepts JSON-compatible object data and resets when the app restarts. A custom store validates data against its declared fields.

db/*.ts

TypeScript JavaScriptconfig/db.tsconfig/db.js
import sqlite from "@primate/sqlite";

export default sqlite();