12 — Schemas¶
Purpose¶
Pin the shape and resolution rules of config.json/config.yaml and the export
conventions of package.json — with the complete field catalogue migrated from
the template CONFIG.md, so it can be summarized without loss.
Sources: qcobjects-new-app CONFIG.md + config.json (v2.4.40-ts); core
README §§ CONFIG, Processor, backend settings.
Machine schema: schemas/config.schema.json; fixtures: schemas/examples/*.json.
Scope¶
config.json (runtime truth) and package.json (distribution contract).
Config file precedence (normative)¶
- The CLI runtime reads ONLY
config.json(defaultsettings.ts,JSON.parseon<projectPath>/config.json) — no YAML branch exists anywhere insrc/. A shippedconfig.yamlis INERT until a YAML loader lands; since the template ships both files with identical content, JSON silently governs. CONFIG.set('useConfigService',true)(or equivalent) enables file-backed settings.- Config files are STANDALONE — no overlay/merging mechanism exists. Minimal
configs (
{"autodiscover":true},{"documentRoot":"…browser/"}) are valid; absent keys fall back to built-ins (documentRoot→<cwd>/public, ports/TLS required only when serving). The schema accordingly requires NOTHING — every key is optional with documented fallbacks.
General fields (normative catalogue)¶
devmode:info|debug|warn|error.autodiscover,autodiscover_libs,autodiscover_commands,autodiscover_handlers: booleans. Absent fromconfig.jsonmeans the CLI built-in defaults apply — which turn ONautodiscover,autodiscover_commands, andautodiscover_handlers(onlyautodiscover_libsdefaults off). To actually disable autoload, the config MUST set the flagsfalseexplicitly; see the autoload contract in 05-cli. (Drift on record: the canonical template ships all three astruewith noautodiscover_libskey — maximal autoload. New apps SHOULD still least-privilege to explicitfalse.)documentRoot(e.g."$config(projectPath)public/"),documentRootFileIndex(defaultindex.html),cacheControl(e.g.max-age=31536000).relativeImportPath(e.g.js/packages/),componentsBasePath.serverPortHTTP/serverPortHTTPS(e.g.'8080'/'8443';process.env.PORToverrides ONLY on legacy/GAE servers — the default HTTP/2start()ignores it).useLocalSDK(local vssdk.qcobjects.dev),useLegacyHTTP,enableShellCommands(CLI defaulttrue; templates SHOULD setfalseunless shell commands are required).useTemplate(CLI defaultfalse):trueenables server-side rendering of.html/.tpl.htmlthroughFileDispatcher(see 02-architecture § Rendering model).private-key-pem/private-cert-pem(e.g."$config(domain)-privkey.pem").domain,certificate_provider, plus server-sidebasePath(read for chdir, set only via config — never defaulted),projectPath(defaults to cwd). (dataPath, e.g./etc/qcobjects/data/, appears in examples but in NO runtime read path — do not rely on it.)
Backend fields (normative catalogue)¶
backend.db_engine{name, databaseName}—$ENV(ENGINE_NAME)/$ENV(DATABASE_NAME)(e.g.sqlite3/admin.db).backend.auth{enabled, defaultUser, defaultPasswd, microsoftapikey, googleapikey}— all secret values via$ENV(...).backend.routes[]— each REQUIRESname,path(regex),microservice; MAY carrydescription,redirect_to,responseHeaders,cors.allow_origins, and a siblingheaderskey (used by real template routes, distinct fromresponseHeaders). Route keys fall in two tiers:- VERIFIED (read site in pinned source): all of the above +
supported_methods(static microservice gating). - OBSERVED, read site in external packages (do NOT rely on framework
behavior; verify against the handler that owns the route):
entity(generic register/list microservices dispatching per entity value),response(inline body served by mockup-style microservices, e.g. OAuth2 token stub),proxyServiceClass+proxy_methods(proxy microservices delegating to a named Service class), route-levelmethod(verb constraint on/saveplayer-style routes), and microservice names with no pinned-source implementation (mockup,proxy,sdk.forbiddenfor deny rules like^/node_modules.*$,openapi.json|yaml,helloworld, PHP bridges). backend.interceptors[]— server-lifecycle plugins, loaded by all three servers at boot: each entry imports itsmicroservicepackage, instantiates<microservice>.Interceptorwith{domain, basePath, projectPath, interceptor, server}(note the live SERVER handle — this is how socket.io and similar layers attach), and pushes the instance tointerceptorInstances. Entries carryname,description,microservice,responseHeaders(proof: video-streamingStart Streamingentry).- Unknown keys pass through inert — with one verified exception.
config.jsonMAY carry app-private keys (e.g. top-leveliceServersfor WebRTC STUN);CONFIG.getserves any key, but the server acts ONLY on keys it reads. The exception: route-levelsupported_methodsIS consumed — by the static microservice (src/backend/backend-microservice-static.ts), which allows delivery only when the request method is"*"-matched or case-insensitively listed (absent = allowed). Do NOT assume any OTHER unknown key takes effect; verify the read site first. package{source{backend,frontend}, build, dist}for packaged builds.
Placeholder resolution (normative)¶
$ENV(VAR)→ environment (Node/CLI/Collab only); a missing one-arg var resolves to EMPTY STRING silently (the CLIENVshim returns"") — never a boot error. Two-arg form$ENV(VAR,default)falls back todefault(e.g."$ENV(DOMAIN,localhost)","$ENV(DEVMODE,info)"); empty default ($ENV(OPENAI_API_KEY,)) means empty string.$config(key)→ sibling key or derived value ($config(domain),$config(projectPath)) — all environments.- Custom
$NAME(args)viaProcessor.setProcessor(fn)(non-arrow;thisis the handler; reach$ENVasthis.processors.ENV(arg)). - Encrypted
config.jsonsupported; decoding transparent toCONFIG.get. - Inline i18n dictionary:
use_i18n:true+lang+i18n.messages[]({en,es}pairs) embeds translations directly in config — read by core (Component.tsgates onCONFIG.get("use_i18n")). Prefer for small static dictionaries; the SDKi18n_messagespackages remain the mechanism for large/dynamic catalogues. - Namespaced custom blocks are the norm for app-private settings:
stripe{…},firebaseclient{…},jira{domain,username,auth_token,project},frontend{credentials{…}},backend.credentials{…},puzzleTimeoutSeconds,backendTimeout— any shape, served byCONFIG.get, never interpreted by the framework. Secrets inside them MUST still come from$ENV(...), never literals (see security note in Verification).
package.json contract (normative)¶
Framework packages MUST declare main (CJS), module (ESM), browser,
types, and an exports map covering ., ./package.json, and per-module
subpaths. Dual cjs/esm + types output is mandatory.
(KNOWN VIOLATIONS at v2.5.158: five entries point at ./public/mjs/*.mjs
but public/mjs/ is never produced — fix to ./public/esm/; "./types/*"
maps a non-existent top-level types/ — real declarations are the single
public/types/index.d.ts. The app template has NO exports map at all —
the contract binds framework packages until the template complies.)
Schema changes MUST be backward compatible within a major line or MUST bump the
major and document migration in the spec + changelog.
Verification¶
npx ajv-cli validate -s schemas/config.schema.json -d <any fixture>passes for every template and test fixture (CI enforces overschemas/examples/).- A
filenamewith multiple dots (e.g.my.page.html) is misclassified byfile_extension()(first-dot, not last) — keep template basenames single-dot. CONFIG.mdfield list ⊆ this catalogue (this spec is the superset of record).- ⚠️ SECURITY: never commit literal secrets in
config.json(passwords, API keys, tokens) and never paste real configs into chats/logs — all secrets belong behind$ENV(...), including reference-looking examples, which MUST be rotated before any production use.