Config reference
The project file that configures Nub's runtime, dependency installs, and temporary package runs.
Put nub.jsonc at the project root — the workspace root in a monorepo. Every project field it takes, with its full value set:
{
"$schema": "https://nubjs.com/schema/latest.json",
// ── runtime — file runs, scripts, and watch mode ──
"preload": ["./setup.ts", "tsx/esm"], // paths or bare specifiers, in order
"nodeOptions": ["--enable-source-maps"], // any flag NODE_OPTIONS accepts
"v8Flags": ["--stack-size=2000"], // the full V8 set, via the command line
"nodeCompat": false, // true = disable all Nub augmentation
"envFile": [".env", ".env.local"], // true = cascade | false = none | "varlock"
// text | jsonc | json5 | toml | yaml | ts | tsx | jsx
"loader": { ".graphql": "text" },
"verifyDeps": "warn", // warn | error | true | false
"tsconfig": "./tsconfig.runtime.json", // paths, conditions, transform defaults; no type checking
"conditions": ["development"], // additional; matched before "default"
"jsx": "react-jsx", // react | react-jsx | react-jsxdev
"jsxImportSource": "preact", // automatic runtime package
"jsxFactory": "createElement", // classic runtime factory
"jsxFragmentFactory": "Fragment", // classic runtime fragment factory
"decorators": "legacy", // legacy TypeScript decorator semantics
"emitDecoratorMetadata": true, // design-type metadata for legacy decorators
// ── installs — layout everywhere; release gates under Nub identity ──
"install": {
"linker": "global-virtual-store", // global-virtual-store | isolated | hoisted
"publicHoist": ["@types/*"], // name patterns; ["*"] for all
"minimumReleaseAge": "3d", // <int><s|m|h|d|w>; default 24h, "0s" off
"minimumReleaseAgeExclude": ["@company/*"]
}
}Nub finds the nearest file by walking up from the command's working directory. The file uses JSON with comments and trailing commas, and an unknown key or invalid value stops the command with an error. Paths resolve from the directory containing nub.jsonc, not the shell's working directory.
Setting a field
Every field is addressable by its dotted path.
nub config set install.linker hoisted # ./nub.jsonc
nub config set --global install.linker hoisted # ~/.config/nub/nub.jsonc
nub global config set install.linker hoisted # equivalent prefix formThe full command surface is under nub config.
Precedence
Every setting resolves through the same chain, most specific first:
- Command-line option —
nub --node app.ts - Environment variable —
NODE_COMPAT=1 - Project
nub.jsonc— the nearest file walking up - Global
nub.jsonc— the same field, for personal defaults everywhere - Built-in default
Both files take the same fields, except dlx, which is global-only. Run nub config path to print the global file Nub will actually read, resolved in this order:
$XDG_CONFIG_HOME/nub/nub.jsonc— when that variable is set~/.config/nub/nub.jsonc— every platform, Windows included%APPDATA%\nub\nub.jsonc— Windows service andSYSTEMaccounts, whose profile lives under the system root
The global file is read leniently, because it applies to every project on your machine: an unrecognized section is ignored, and a bad value skips the whole file rather than failing the command. Only the project file, which is checked in and shared, fails loud.
Relative paths
Three fields take paths: preload, envFile, and tsconfig. A relative path resolves against the directory of the file that set it — not your working directory.
For a project nub.jsonc that is the directory the file sits in — in a monorepo, wherever the nearest one lives, which need not be the package you are running from. For the global file it is your config directory, which is where the rule starts to surprise people.
{
// ...
"envFile": [".env.shared"] // resolves to ~/.config/nub/.env.shared
}Set a path globally and you almost certainly meant a fixed location, so write it absolute or lead with ~/.
{
// ...
"envFile": ["~/secrets/.env"]
}There is no way to write a global path meaning "each project's own directory". Put project-relative paths in the project file.
JSON Schema
Nub publishes a JSON Schema for nub.jsonc. The same document describes the project file and your global config, so either file can point at it for editor completion and validation.
{
"$schema": "https://nubjs.com/schema/latest.json",
// ...
}Schemas are served from https://nubjs.com/schema/:
| URL | Role |
|---|---|
latest.json | Rolling — always matches the newest release; new fields show up in the schema as they ship |
https://nubjs.com/schema/v0.7.json | Pinned to the 0.7 minor series — use when you want the schema to stop drifting under a checkout |
Each minor series gets one file (v0.6.json, v0.7.json, …). On every release bump Nub snapshots the rolling schema into that series' file; patch releases within 0.7.x keep rewriting v0.7.json only when the published field surface changes. Older series stay published for long-lived branches. The schema index lists every file on disk.
Nub does not read $schema when it runs your code; the key is for editors only.
Runtime
The top-level fields configure file runs, package scripts, and watch mode.
preload
Load one or more modules before the entry file.
{
// ...
"preload": [
"./instrumentation.ts",
"./register-hooks.mjs"
]
}globalThis.performance.mark("app-start");An entry may also be a bare package specifier, resolved the way an import in your project would be.
{
// ...
"preload": ["tsx/esm", "@company/telemetry/register"]
}A bare specifier resolves through the package's exports map, so conditions steers which file it loads.
{
// ...
"conditions": ["development"],
"preload": ["@company/telemetry/register"]
}{
"exports": {
"./register": {
"development": "./register.dev.js",
"default": "./register.js"
}
}
}nodeOptions
Pass Node options to runtime commands.
{
// ...
"nodeOptions": [
"--enable-source-maps",
"--stack-trace-limit=50"
]
}The config command addresses this field as runtime.nodeOptions, because the bare nub config set nodeOptions spelling writes pnpm's nodeOptions setting in .npmrc.
nub config set runtime.nodeOptions '["--enable-source-maps"]'Read the full docs on pnpm.io for that .npmrc setting.
v8Flags
Pass V8 flags without adding them to every command.
{
// ...
"v8Flags": [
"--expose-gc",
"--stack-size=2000"
]
}These flags reach Node on its command line rather than through NODE_OPTIONS, so the whole V8 flag set is available — including the flags NODE_OPTIONS rejects, such as --stack-size and --no-opt. Use nodeOptions for Node's own options, which do travel through NODE_OPTIONS.
Nub applies them to every Node process it starts, including the ones your package scripts launch as node. A script that runs Node through a hardcoded absolute path instead does not get them.
nodeCompat
Use plain Node behavior while keeping Nub's Node version selection.
{
// ...
"nodeCompat": true
}This is the project-wide form of the --node escape hatch. Nub does not load its runtime hooks, environment files, preloads, custom loaders, or conditions.
envFile
Keep automatic environment-file discovery with true.
{
// ...
"envFile": true
}Load no environment at all with false. This covers Varlock too, so a project with a .env.schema hands its program an empty environment.
{
// ...
"envFile": false
}Name exact files in an array, in order. Later files replace earlier values.
{
// ...
"envFile": [
".env",
".env.development",
".env.local"
]
}A path always goes in an array, including a single one. A bare string is a mode name, and the only one is "varlock".
{
// ...
"envFile": [".env.development"]
}Paths expand ${VAR} and $VAR against the environment, which a config file never gets from the shell, and resolve as described under relative paths.
{
// ...
"envFile": [".env", ".env.${APP_ENV}"]
}APP_ENV=staging nub app.ts # reads .env and .env.stagingSetting envFile takes precedence over Varlock. A .env.schema file is a signal Nub infers ownership from; an envFile value is a project stating what it wants, so the value wins and Varlock stays out of the chain. The --env-file and --no-env-file flags do the same.
This holds at every scope, your global config included. A .env.schema decides the environment only when nothing else does, so setting envFile globally reaches into schema projects too.
Hand the environment to Varlock explicitly with "varlock". That is how a project keeps Varlock when your global config turns environment files off.
{
// ...
"envFile": "varlock"
}loader
Teach Nub an extension it does not already know, by mapping it onto a loader it has.
{
// ...
"loader": {
".graphql": "text", // import a schema as a string
".rules": "yaml", // a project file that happens to be YAML
".view": "tsx" // project source under a name of your own
}
}Nub already imports .txt, .jsonc, .json5, .toml, .yaml, and .yml, along with the TypeScript and JSX extensions, so none of those need an entry here.
The supported loader names are:
text
jsonc
json5
toml
yaml
ts
tsx
jsxAn entry may also point an extension Nub already knows at something else — ".ts": "text" imports TypeScript sources as strings, and ".ts": "tsx" turns JSX on for them. One pairing is rejected: ts on .tsx or .jsx. Those extensions are always parsed as JSX, so the entry could not take effect.
verifyDeps
Warn when installed dependencies may be stale.
{
// ...
"verifyDeps": "warn"
}Stop instead of running.
{
// ...
"verifyDeps": "error"
}Skip the check.
{
// ...
"verifyDeps": false
}Nub rejects pnpm's "install" and "prompt" values, which it does not implement.
tsconfig
Choose the TypeScript configuration Nub's runtime reads. Nub uses it for path mapping, custom conditions, and as the fallback source for JSX and decorator settings — not for type checking. Top-level JSX and decorator fields in nub.jsonc override their compilerOptions counterparts.
{
// ...
"tsconfig": "./tsconfig.runtime.json"
}{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}conditions
Note
conditions is also supported as compilerOptions.customConditions in tsconfig.json. Prefer the TypeScript config in TypeScript projects.Add additional package export conditions. Nub passes each entry to Node as --conditions (same as -C on the command line). Node always keeps its built-in conditions (node, import, require, default); your names are extra, and on an exports map they are matched before default.
{
// ...
"conditions": [
"development",
"react-server"
]
}{
"exports": {
".": {
"development": "./development.js",
"default": "./production.js"
}
}
}Nub also applies the customConditions your TypeScript config declares, so a project already using them does not repeat itself here.
{
"compilerOptions": {
// types resolve through the package's "source" export
"customConditions": ["source"]
}
}TypeScript uses them to resolve types, and Nub passes them to Node as well — so the file the type checker followed is the file that runs. A condition declared in the base config a package extends counts.
jsx
Note
jsx, jsxImportSource, jsxFactory, and jsxFragmentFactory are also supported in tsconfig.json. Prefer the TypeScript config in TypeScript projects.Configure JSX without adding a TypeScript config. The accepted modes all emit JavaScript that Nub can execute directly: react, react-jsx, and react-jsxdev.
{
// ...
"jsx": "react-jsx",
"jsxImportSource": "preact"
}The automatic runtime uses jsxImportSource as its package. The classic runtime uses jsxFactory as its element function and jsxFragmentFactory as its fragment value.
{
// ...
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}decorators
Note
compilerOptions.experimentalDecorators in tsconfig.json. Prefer the TypeScript config in TypeScript projects.Enable TypeScript's legacy decorator transform.
{
// ...
"decorators": "legacy"
}emitDecoratorMetadata
Note
emitDecoratorMetadata is also supported in tsconfig.json. Prefer the TypeScript config in TypeScript projects.Emit the design-type metadata used by decorator-based dependency injection and ORM libraries. This setting takes effect with legacy decorators enabled.
{
// ...
"decorators": "legacy",
"emitDecoratorMetadata": true
}install
The install block takes four fields. Its layout fields apply in every project; its release-age fields apply only when Nub is the project's package manager.
| behavior | fields | where it applies |
|---|---|---|
node_modules layout | linker, publicHoist | Every project, including npm, pnpm, Yarn, and Bun incumbents |
| Version cooling window | minimumReleaseAge, minimumReleaseAgeExclude | Nub-identity projects |
Under an npm, pnpm, Yarn, or Bun incumbent, Nub names each release-age field it skipped.
nub: `install.minimumReleaseAge` ignored — it is nub-native install config, and this project resolves as pnpm. Run `nub pm use nub` to make nub the project's package manager.install.linker
Pick the dependency layout. Name a strategy as a string when you want its defaults.
{
// ...
"install": {
"linker": "hoisted"
}
}| strategy | layout |
|---|---|
global-virtual-store | one machine-shared store, symlinked into each project. The default outside CI — see shared vs per-project store. |
isolated | a store inside the project, still symlinked. |
hoisted | flat real directories, the npm and Yarn shape. |
Read the full docs on pnpm.io.
Two strategies take an extra key, written in the object form: global-virtual-store takes eject, and isolated takes hoist. Neither hoisted nor pnp takes one, so the object form for those carries strategy alone.
{
// ...
"install": {
"linker": {
"strategy": "global-virtual-store",
"eject": ["electron", "@company/native-*"]
}
}
}Ejected packages are written into the project as real directories instead of linked out of the shared store. Reach for it when a package resolves paths relative to its own location on disk, which native builds and self-extracting binaries tend to do.
This list adds to the set Nub already ejects — the packages known to break when their real path sits outside the project. It cannot shrink that set, so "eject": [] leaves the built-in list in force rather than turning ejection off.
Under isolated, hoist fills the hidden fallback tree that your dependencies reach when they import something they never declared. It takes true, false, or a list.
{
// ...
"install": {
"linker": {
"strategy": "isolated",
"hoist": ["*eslint*", "@types/*"]
}
}
}Setting false is strict: an undeclared import fails instead of resolving. Setting true matches everything, which is pnpm's default.
Writing a knob under the wrong strategy is an error, and the message names the strategy that takes it.
`install.linker.hoist` in nub.jsonc: not valid with `strategy: "global-virtual-store"` — it
configures the "isolated" layout. Either switch to `strategy: "isolated"` or drop this key.The strategy "pnp" is reserved for future Plug'n'Play support. An install that names it is rejected until support lands.
nub: `install.linker: "pnp"` is reserved and not supported yet [ERR_NUB_CONFIG_UNSUPPORTED]install.publicHoist
Put packages in the project's top-level node_modules without declaring them. Use it for tools that resolve from the project root rather than through a dependency's own lookup — TypeScript finding @types/*, a linter loading its plugins.
{
// ...
"install": {
"publicHoist": ["@types/*", "*eslint*"]
}
}Read the full docs on pnpm.io.
A single * lifts everything, matching pnpm's shamefully-hoist. Every package then resolves every other one, so the layout's isolation is gone.
{
// ...
"install": {
"publicHoist": ["*"]
}
}The field sits outside linker because it writes the project's own node_modules, which exists under every strategy.
install.minimumReleaseAge
Reject versions published too recently. Nub already enforces a 24-hour window by default — see cooling window — so this field widens or narrows an existing gate rather than turning one on. Set it to "0s" to disable it.
{
// ...
"install": {
"minimumReleaseAge": "3d"
}
}Read the full docs on pnpm.io.
Durations use an integer plus one unit.
30s # seconds
20m # minutes
12h # hours
3d # days
2w # weeksPositive durations are stored in whole minutes and rounded up: 1s through 1m become one minute, while 0s still disables the gate.
The gate fails closed: when no version in a range is old enough, the install stops. Under pnpm the resolution falls back to an older version by default; Nub does not.
Exclude selected packages when they must update immediately.
{
// ...
"install": {
"minimumReleaseAge": "3d",
"minimumReleaseAgeExclude": [
"@company/*",
"typescript"
]
}
}dlx
The dlx block configures temporary package runs through nubx, nub dlx, and nub x. Nub reads it from the global file only — reaching the registry is a decision about your machine, not about one repository — so a project nub.jsonc carrying a dlx block stops the command.
Set consent to ask before an implicit nubx registry fetch.
{
// ...
"dlx": {
"consent": "prompt"
}
}Disable implicit registry fetches.
{
// ...
"dlx": {
"consent": "never"
}
}Explicit nub dlx runs still work because the command itself supplies consent.
nub config
Create a commented project file with every supported project field. Only the schema URL is active. Run it from anywhere in a workspace and the file lands at the workspace root, which every member reads.
nub config initCreate the global file instead to include machine-wide fields such as dlx.consent.
nub config init --global
# equivalently: nub global config initThe command refuses to replace an existing file. Read and write individual fields with the remaining config commands.
nub config set nodeCompat true # writes ./nub.jsonc
nub config get install.linker # hoisted
nub config delete install.minimumReleaseAge
nub config path # the global file's locationEach field is written whole. Pass arrays and objects as one shell-quoted JSON argument, with JSON keys and strings in double quotes.
nub config set preload '["./setup.ts","tsx/esm"]'
nub config set loader '{".graphql":"text",".rules":"yaml"}'Scalar values need no JSON quoting. A union field uses JSON only for its array or object form.
nub config set install.linker '{"strategy":"isolated","hoist":["@types/*"]}'Values are checked against the same rules that read the file, so a rejected write leaves it untouched.
$ nub config set install.minimumReleaseAge 3
nub: `install.minimumReleaseAge` in /app/nub.jsonc: invalid duration `3` — expected an integer followed by a unit s|m|h|d|w (e.g. "3d")Comments, blank lines, and key order survive an edit.
Writes go to the project file, and --global sends them to the global one instead. Fields under dlx always go to the global file, with or without the selector.
nub config set --global envFile false # personal default, everywhere
nub global config set envFile false # equivalent prefix form
nub config set dlx.consent never # global; the selector is redundantReads take the project file first and fall back to the global one. Structured values print as compact JSON so they can be passed back to set; scalar strings print without JSON quotes. An unset field prints undefined, and --json prints the exact stored value.
nub config get --local nodeCompat # project file only
nub config get preload --json # ["./setup.ts","tsx/esm"]A key that is not a field of this file reads and writes .npmrc.
Corepack-style shims
An opt-in for muscle memory. Install the shims and a bare pnpm, npm, or yarn command routes through Nub to the package manager your project pins, with no extra Node process in front.
Deployment
Deploy Nub projects through GitHub Actions, the official container image, or the installer and npm package used by managed builders.