LPM-cli

lpm.json

Canonical schema reference for the lpm.json project file.

lpm.json sits next to package.json and provides LPM-specific configuration that doesn't fit npm's manifest shape — runtime pinning, task graph, dev services, local-domain proxy config, tunnel domain, env-file mapping, env-var schema, and multi-registry publish settings.

The file is optional. Every field has a default. When lpm.json is missing, LPM falls back to package.json semantics (scripts, engines, etc.).

This page is the schema-by-field reference. For a usage-oriented overview of what lpm dev reads from lpm.json, see lpm dev.

Editor autocomplete

LPM publishes a JSON Schema for lpm.json at https://lpm.dev/schemas/lpm.json — auto-derived from the typed Rust struct that the CLI consumes. Add a $schema line to get inline validation and field completion in any editor that supports JSON Schema (VS Code, Cursor, JetBrains):

lpm.json
{
  "$schema": "https://lpm.dev/schemas/lpm.json",
  "runtime": { "node": ">=22.0.0", "bun": "1.3.14" }
}

Or emit the schema yourself from the locally-installed CLI:

lpm schema lpm.json                       # to stdout
lpm schema lpm.json -o lpm.schema.json    # to a file

Both lpm schema lpm.json and the published copy are regenerated from the same Rust struct that parses your manifest at install time, so they describe the surface the CLI actually accepts. The published copy is updated alongside CLI releases — between releases, lpm schema lpm.json is the source of truth for whichever version is on your machine.

When stdout is an interactive terminal, schema output may use syntax color. Redirected or piped output remains plain pretty-printed JSON, including when color is forced globally.

Top-level shape

lpm.json
{
  "$schema":      "https://lpm.dev/schemas/lpm.json",
  "vault":        "<uuid-v4>",
  "runtime":      { "<runtime>": "<semver>" },
  "tools":        { "<tool>": "<version>" },
  "https":        true,
  "cert":         { "extraPermittedDns": ["myapp.local"], "allowPublicDns": false },
  "tunnel":       { "domain": "<full-domain>" },
  "proxy":        { "host": "<local-hostname>", "port": 443, "httpRedirect": true },
  "env":          { "<script>": "<.env-file>" },
  "envSchema":    { "vars": { "<NAME>": { "required": true, "format": "url" } } },
  "environments": { "<name>": { "extends": "<base>", "file": "<.env-file>" } },
  "tasks":        { "<name>": { /* TaskConfig */ } },
  "services":     { "<name>": { /* ServiceConfig */ } },
  "publish":      { /* PublishConfig */ }
}

Every top-level field is optional and defaults to empty / null.

runtime

{ "runtime": { "node": ">=22.0.0", "bun": "1.3.14" } }
FieldTypeDefaultNotes
nodestringManaged Node version spec. Exact versions, prefixes, semver ranges, latest, and lts are accepted.
bunstringManaged Bun version spec. Exact versions, vX.Y.Z, bun-vX.Y.Z, latest, prefixes, and semver ranges are accepted. lts is rejected because Bun has no LTS channel.

runtime.node takes precedence over package.json > engines.node and .nvmrc. runtime.bun is the only Bun runtime detection source; package.json > engines.bun is not enforced. Both are auto-installed via lpm use.

When both runtimes are selected, script PATH is prepended as node_modules/.bin, managed Node bin, managed Bun bin, inherited PATH. runtime.bun exposes bun to scripts; it does not make lpm run invoke bun run.

tools

{ "tools": { "oxlint": "1.57.0", "biome": "2.4.8" } }
FieldTypeNotes
<tool>stringPin a built-in tool to a specific version. Honored by lpm lint and lpm fmt only — those are the plugin-backed tools.

Only oxlint and biome are plugin-backed today. Any other key (tools.typescript, tools.tsgo, tools.eslint, …) is dropped silently by the resolver — lpm warns once per command invocation that the pin is ignored. To pin the lpm check engines, declare typescript or @typescript/native-preview in package.json > devDependencies like any other project dep.

When the pinned version isn't already in ~/.lpm/cache/, the next invocation of the tool downloads it. Pinned versions verify against the upstream <asset_url>.sha256 sidecar; see Built-in tools for the trust model.

https

{ "https": true }
TypeDefaultNotes
boolnull (unset)When true, lpm dev serves over HTTPS by default. CLI --no-https overrides.

cert

{ "cert": { "extraPermittedDns": ["myapp.local"], "allowPublicDns": false } }
FieldTypeDefaultNotes
extraPermittedDnsstring[][]Extra validated DNS subtrees to permit on the constrained project intermediate. These are policy constraints, not extra browser SANs and not proxy routes.
allowPublicDnsboolfalseAllows public DNS names in extraPermittedDns and local-domain host fields. Leave off for purely local names.

extraPermittedDns entries must be bare multi-label names such as myapp.local; LPM derives both the exact subtree and subdomain subtree for the project intermediate. Use --host or proxy.host / services.<name>.host for actual browser hostnames.

tunnel

{ "tunnel": { "domain": "acme-api.lpm.llc" } }
FieldTypeNotes
domainstringStable tunnel domain (must already be claimed via lpm tunnel claim). Pro/Org only — free users get an ephemeral random domain.

CLI flags: lpm dev --tunnel, lpm dev --domain <NAME>, lpm tunnel <port> <domain>.

proxy

{ "proxy": { "host": "app.localhost", "port": 443, "httpRedirect": true } }
FieldTypeDefaultNotes
hoststringFriendly top-level local hostname registered by lpm dev when the proxy daemon has an HTTPS listener. lpm dev also prepares constrained project cert-chain coverage and updates the hosts file when this host is not under .localhost.
portnumber443HTTPS proxy listen port used by lpm proxy start when no explicit listener flags are passed. 0 lets the OS pick a free port.
httpRedirectbooltrueWhether lpm proxy start also binds an HTTP-to-HTTPS redirect listener on port 80 when no explicit listener flags are passed.

proxy.host and services.<name>.host are normalized to lowercase, must use a local TLD (.localhost, .test, .local, .internal, .home.arpa) unless cert.allowPublicDns is true, and cannot duplicate another local-domain host in the same file.

When local-domain config is present and no daemon is running, lpm dev starts lpm proxy start --detach automatically. With no explicit listener flags, that start path uses proxy.port and proxy.httpRedirect. With a running HTTPS listener, lpm dev prepares the project certificate chain for configured hosts through the normal trust consent flow, writes managed hosts-file entries for hosts that are not localhost / *.localhost, registers routes against final assigned service ports, and releases both route and hosts-file entries when the dev session exits. On Unix, LPM uses sudo for the system hosts file when the current process lacks permission; on Windows, it asks for Administrator elevation through UAC. Use lpm hosts clean to remove orphaned managed blocks after interrupted sessions. The TLS daemon also prepares or refreshes project certificate chains during route registration, but it does not install the root CA into the trust store. The startup banner shows configured proxy hosts, exact route lines print after registration, and dashboard service rows include the host URLs. Proxy-only cert preparation does not inject framework HTTPS env vars into the app; app HTTPS remains controlled by https / --https.

vault

{ "vault": "7f3a1e2c-5b9d-4a8f-b6c1-9b1d2e3f4a5b" }
TypeNotes
stringUUID v4. Stable opaque identifier that links this project to a vault row on lpm.dev. Generated and written back by the CLI on first lpm env use if missing. Commit it to git — it's a pointer, not a secret.

The value is path-traversal-validated: empty strings, /, \, .. substrings, leading ~, control characters, and absolute-drive prefixes are rejected (the value is joined into ~/.lpm/vaults/<id>.enc on machines using the file fallback). Removing the field regenerates a fresh UUID on next use, which decouples the working directory from the previous vault without deleting server-side data.

LPM_VAULT_ID overrides per-invocation. See Secrets vault — Per-project identity.

vaultSync

{
  "vaultSync": {
    "personalVersion": 7,
    "orgVersions": { "acme": 12 }
  }
}
FieldTypeNotes
personalVersionintLast known personal cloud-vault version this checkout has seen.
orgVersions.<slug>intLast known org-shared cloud-vault version per org slug.

Written by lpm env push / lpm env pull for conflict-detection (CAS) against the server. Don't edit by hand; mismatches surface as 409 VaultConflictError and the CLI tells you to pull, reconcile, and re-push.

env

{
  "env": {
    "dev":     ".env.development",
    "staging": ".env.staging",
    "prod":    ".env.production"
  }
}

Map of script name → env file. lpm run dev loads .env.development; lpm run staging loads .env.staging. CLI --env=<MODE> overrides per-invocation (and applies to lpm dev too).

envSchema

{
  "envSchema": {
    "vars": {
      "DATABASE_URL": { "required": true, "format": "url" },
      "API_KEY":      { "required": true, "secret": true },
      "LOG_LEVEL":    { "default": "info", "pattern": "^(trace|debug|info|warn|error)$" }
    }
  }
}

Schema for env-var validation. Drives the env check that runs before lpm run, lpm dev, and lpm exec. Per-var fields:

FieldTypeNotes
requiredboolFail if not set
formatstringBuilt-in validator. One of: url, email, port, boolean, integer, hostname, ip
patternstringRegex the value must match
defaultstringDefault value if unset
secretboolTreat as sensitive — masked in logs and lpm env list

Skip the check per-invocation with --no-env-check.

environments

{
  "environments": {
    "base":    { "file": ".env" },
    "staging": { "extends": "base", "file": ".env.staging" },
    "preview": { "extends": "staging", "file": ".env.preview" }
  }
}

Named environment definitions with inheritance. Useful when you have many .env.* variants and want to avoid duplicating shared keys. extends chains resolve transitively.

tasks.<name>

{
  "tasks": {
    "build": {
      "command": "tsup",
      "dependsOn": ["^build"],
      "cache": true,
      "outputs": ["dist/**"],
      "inputs": ["src/**", "package.json"],
      "env": "production"
    }
  }
}
FieldTypeDefaultNotes
commandstringfalls back to package.json > scripts.<name>Override the script body
dependsOnstring[][]Other tasks that must finish first. "build" = same package; "^build" = the same task in upstream workspace deps.
cacheboolfalseEnable task caching. Requires outputs.
outputsstring[][]Globs of files this task produces (e.g. ["dist/**"]). Required when cache: true.
inputsstring[]sensible defaults*Globs that invalidate the cache.
envstringEnv mode for this task ("production" loads .env.production).

*Default inputs: src/**, lib/**, app/**, pages/**, components/**, package.json, tsconfig.json, tsconfig.*.json, *.config.{js,ts,mjs}.

remoteCache

{
  "remoteCache": {
    "enabled": true,
    "team": "acme",
    "url": "https://lpm.dev/v8",
    "signature": true,
    "readOnly": false,
    "env": {
      "include": ["CI", "NODE_ENV"],
      "exclude": ["*_TOKEN", "*_SECRET", "DATABASE_URL"]
    }
  }
}

Hosted cache settings for tasks that already opt in with tasks.<name>.cache: true and outputs. LPM checks the local task cache first, then the hosted cache, then runs the task and uploads the result when the run succeeds.

FieldTypeDefaultNotes
enabledboolfalseEnable hosted cache reads and writes
teamstringpersonal namespaceOrganization slug/team namespace
urlstringconfigured registry + /v8Remote cache endpoint. Hosts outside the configured registry origin require LPM_REMOTE_CACHE_TOKEN and LPM_REMOTE_CACHE_SIGNATURE_KEY.
signatureboolfalseRequire LPM_REMOTE_CACHE_SIGNATURE_KEY; downloads without a valid HMAC tag are treated as misses. Third-party cache hosts always require a signing key.
readOnlyboolfalseRead hosted artifacts but skip uploads
env.includestring[][]Variable-name patterns explicitly allowed for remote uploads
env.excludestring[][]Variable-name patterns that block remote uploads
env.allowSecretsboolfalsePermit secret-looking env var names after project opt-in

Secret-looking loaded env vars (TOKEN, SECRET, PASSWORD, DATABASE_URL, *_KEY, etc.) block remote uploads by default. If a blocked value appears in captured stdout/stderr, the upload is also skipped. Local task caching still works.

services.<name>

{
  "services": {
    "db":  { "command": "docker compose up postgres", "readyPort": 5432, "readyTimeout": 60 },
    "api": {
      "command": "node server.js",
      "port": 4000,
      "dependsOn": ["db"],
      "env": { "DATABASE_URL": "postgres://localhost:5432/myapp" }
    },
    "web": { "command": "next dev", "port": 3000, "primary": true, "host": "web.app.localhost" }
  }
}
FieldTypeDefaultNotes
commandstringrequiredShell command to run
portnumberPort the service listens on. If omitted for a host-only service, lpm dev auto-assigns and persists one
dependsOnstring[][]Services that must be ready before this one starts
readyPortnumberfalls back to portTCP port to poll for readiness
readyUrlstringHTTP URL to poll for readiness — first 2xx wins
readyTimeoutnumber30Seconds to wait before failing
envobject{}Extra env vars injected into this service only
restartboolfalseAuto-restart on crash with exponential backoff
primaryboolfalseReceives --https / --tunnel / --network flags from lpm dev, plus the browser-open
hoststringFriendly local hostname registered by lpm dev against the final assigned service port when the proxy daemon has an HTTPS listener. lpm dev also prepares constrained project cert-chain coverage and updates the hosts file when the host is not under .localhost. With port, localhost:<port> keeps working; without port, lpm dev injects the assigned port as PORT.
cwdstringproject rootWorking directory, relative to project root

publish

{
  "publish": {
    "registries": ["lpm", "npm"],
    "lpm":    { "name": "@lpm.dev/owner.pkg" },
    "npm":    { "name": "@scope/pkg", "access": "public", "tag": "latest", "otpRequired": true },
    "github": { "name": "@owner/pkg", "access": "public" },
    "gitlab": { "name": "pkg", "access": "public", "projectId": "12345", "registry": "https://gitlab.com" }
  }
}

Top-level

FieldTypeDefaultNotes
registriesstring[]["lpm"]Targets a single lpm publish invocation hits. Values are lpm, npm, github, gitlab, or an https:// custom registry URL. CLI flags (--lpm, --npm, --github, --gitlab, --publish-registry) override.

publish.lpm

FieldTypeNotes
namestringOverride the lpm.dev package name. Must match @lpm.dev/owner.pkg.

publish.npm

FieldTypeDefaultNotes
namestringpackage.json > nameOverride the npm package name (e.g., "@scope/pkg"). Required if package.json > name starts with @lpm.dev/.
access"public" | "restricted"scope-based*Visibility
tagstring"latest"dist-tag for the published version
registrystringhttps://registry.npmjs.orgCustom npm-compatible registry URL. npm Trusted Publishing/OIDC is attempted only for the default npm registry; custom registries use token auth.
otpRequiredboolfalsePrompt for OTP before the first publish attempt (saves a round-trip)

*Scoped packages default to "restricted", unscoped to "public".

publish.github

FieldTypeNotes
namestringOverride the GitHub Packages name. Must be scoped (@owner/pkg).
access"public" | "restricted"Visibility

publish.gitlab

FieldTypeDefaultNotes
namestringOverride the GitLab Packages name
access"public" | "restricted"Visibility
projectIdstringrequiredGitLab project ID — needed to construct the registry URL
registrystringhttps://gitlab.comGitLab instance URL

Precedence with other config

For fields with multiple sources, the precedence chain is:

  1. CLI flag (highest)
  2. lpm.json
  3. package.json (where applicable — engines, scripts)
  4. ~/.lpm/config.toml (user-level, where applicable)
  5. Built-in default (lowest)

See also