| name | extends-config |
| description | How bos.config.json extends chains work, deep merge semantics, resolved config lifecycle, env-specific extends, and canonical field ordering. Use when debugging extends inheritance, configuring per-environment parents, understanding what dev writes vs publish writes, or reasoning about config merging. |
| metadata | {"sources":"packages/everything-dev/src/merge.ts,packages/everything-dev/src/config.ts,packages/everything-dev/src/shared-deps.ts,packages/everything-dev/src/types.ts"} |
extends & Config Merging
extends Field
The extends field in bos.config.json specifies a parent config to inherit from. Supports two forms:
String (all environments use same parent)
{ "extends": "bos://dev.everything.near/everything.dev" }
Object (per-environment parent)
{
"extends": {
"development": "bos://dev.everything.near/everything.dev",
"production": "bos://dev.everything.near/everything.dev",
"staging": "bos://staging.everything.near/everything.dev"
}
}
Fallback chain: requested env → production → first defined value.
Deep Merge Semantics
Uses defu (with createDefu for custom merge rules):
| Field type | Merge behavior |
|---|
| Scalars (account, domain, repository) | Child overrides parent; parent inherited when child omits |
app.api.shared, app.auth.shared, plugins.*.shared dep entries | Deep merged — child overrides specific keys, parent deps preserved |
plugins | Deep merged — child overrides per-key, parent plugins preserved unless removed |
secrets arrays | Unioned (deduplicated) |
routes arrays | Child replaces parent |
variables | Deep merged per-key |
Null Sentinel Removal
Set a plugin to null to explicitly remove an inherited plugin:
{
"plugins": {
"template": null
}
}
Fixed-Core Tenant Mode
For shared-host tenant setups, extends is both the config inheritance mechanism and the lineage edge between runtimes.
Example:
{
"extends": "bos://pingpayio.near/pingpay.io",
"account": "pizza.pingpayio.near",
"domain": "pizza.com"
}
Use this mental model:
extends says which runtime this one remixes or inherits from
account is the tenant namespace root for this runtime when it is served as a base app
domain is the public ingress for this runtime
- a runtime can be a child in lineage and still become a new tenant root on its own domain
With host env like:
ALLOW_OVERRIDE=ui,plugins.*
TENANT_WHITELIST=pizza.pingpayio.near
ALLOW_UNTRUSTED_SSR=false
Design target for request mapping:
pizza.com -> base runtime bos://pizza.pingpayio.near/pizza.com
chicago.pizza.com -> bos://chicago.pizza.pingpayio.near/pizza.com
Current implementation note:
- the host still applies a single request-scoped tenant overlay on top of one process-wide base runtime
- nested label routing and account-relative tenant derivation are the intended direction, but not the complete runtime behavior today
What the tenant may override today
In fixed-core mode, the host keeps the server core from the active base runtime and only applies request-scoped UI-facing overrides from the child config:
app.ui
- existing
plugins.<id>.ui
- existing
plugins.<id>.sidebar
The tenant config must extend the base BOS runtime. Tenant API/auth overrides and dynamic new plugin IDs are not part of this mode.
SSR behavior
Tenant SSR is gated separately from inheritance:
- if
ALLOW_UNTRUSTED_SSR=true, any valid tenant with SSR config may SSR
- otherwise the tenant account must be listed in
TENANT_WHITELIST
- non-whitelisted tenants fall back to client rendering
Resolved Config: .bos/bos.resolved-config.json
Generated by: bos dev, bos build, syncResolvedSharedDeps()
Gitignored: Yes (inside .bos/)
When bos dev or bos build runs:
- The full extends chain is resolved in memory
- The merged config is written to
.bos/bos.resolved-config.json
bos.config.json is NOT modified during dev
Structure:
{
"_resolved": {
"env": "development",
"resolvedAt": "2026-05-11T...",
"extendsChain": ["bos://dev.everything.near/everything.dev"]
},
"account": "me.near",
"domain": "my.dev",
"app": { ... },
"plugins": { ... }
}
Build configs read resolved config first
All build configs (ui/rsbuild.config.ts, host/rsbuild.config.ts, api/rspack.config.js, plugins/*/rspack.config.js) try .bos/bos.resolved-config.json first, falling back to bos.config.json.
The _resolved metadata is stripped before use.
When bos.config.json IS written
| Command | Writes bos.config.json? | Why |
|---|
bos dev | No | Uses resolved config |
bos build | No | Uses resolved config |
bos publish --deploy | Yes | Snapshot moment — pins production URLs + versions |
bos plugin publish | Yes | Records production URL + integrity |
bos plugin add/remove | Yes | Changes project's own plugin list |
bos sync | Yes | Merges template updates into local config |
Remote host mode (bos->catalog)
When host is remote, syncResolvedSharedDeps() reads versions from bos.config.json and writes them into package.json catalog. No resolved config is written — the remote host reads bos.config.json directly.
_resolved.resolvedAt
The resolvedAt timestamp in .bos/bos.resolved-config.json is metadata for debugging only. It is not used for staleness detection — the CLI always re-resolves on bos dev / bos build.
Circular Extends Detection
If the extends chain contains a cycle (e.g., A→B→A), the resolver detects it during config loading and throws an error listing the cycle path. This prevents infinite recursion during merge.
Detection occurs in resolveExtendsRef() → mergeBosConfigWithExtends(). The extends chain is tracked as a set of visited BOS refs; a duplicate visit triggers the error.
Canonical Field Ordering
BOS_CONFIG_ORDER enforces consistent key order:
extends — always first
account
domain
title
description
testnet
staging
repository
ci
app
plugins
Unknown keys go after known keys. rebuildOrderedConfig() is applied before every write.
API
From packages/everything-dev/src/config.ts:
writeResolvedConfig(configDir, config, env, extendsChain?) — writes .bos/bos.resolved-config.json
loadResolvedConfig(configDir) — reads resolved config, returns BosConfig | null
resolveBosConfigPath(configDir) — returns resolved config path if exists, else bos.config.json
readBosConfigForBuild(configDir) — reads resolved config stripping _resolved, falls back to bos.config.json
From packages/everything-dev/src/merge.ts:
mergeBosConfigWithExtends(parent, child) — deep merge for extends chain
mergeBosConfigWithTemplate(local, template) — merge for sync (local wins)
resolveExtendsRef(extendsField, env) — resolve string|object extends for a given env
rebuildOrderedConfig(config) — enforce canonical ordering
BOS_CONFIG_ORDER — ordered field names