Configuration
This page explains where configuration is loaded, how layers merge, and how to verify the effective values. For your first project, see Quick Start. Work through the complete example below without external services before reading the loading mechanism and individual settings. See Configuration API for exact signatures and all nested fields. Each later code block is an independent fragment to merge into existing configuration, not a replacement for the entire file.
Complete example
Start with a TypeScript project from Quick Start.
It needs dev, build (including --typecheck), and start npm scripts.
Merge the following configs into the corresponding files and add the
diagnostic route. To reproduce these results, use an isolated practice
project with the scaffold's empty bootstrap.ts, so an existing provider
or other profile does not alter the example.
Only fields needed for this verification are explicit; the rest use framework defaults. No external service is connected. See the sections below and Configuration API for the full field reference.
- Clear prior PORT, VEXT_PORT/VEXT_HOST, VEXT_CONFIG, and similar overrides
in the terminal, or use a clean one. Run
npm run dev. Requesthttp://127.0.0.1:8080/config-info: expect 200 anddatacontainingport=8080,logLevel=info, anddocsEnabled=true. This proveslocal.tsapplies in development. - Stop dev, run
npm run build, thennpm start. Requesthttp://127.0.0.1:3000/config-info: expect 200 withport=3000,logLevel=warn, anddocsEnabled=false. This proves the production override applies andlocal.tsdoes not. - Stop the service and run
npm start -- --port 3100. Request port 3100 and expectport=3100, verifying CLI override priority. Stop this service after verification.
Expose only these three non-sensitive diagnostic fields. Do not return the
whole app.config as an application endpoint. The listening host may be
0.0.0.0 or ::; it is not the public base URL. Deployment behind a
proxy or CDN determines that address, which cannot be inferred from host and
port alone.
VextJS merges multiple configuration layers to support environment-specific overrides and built-in framework settings.
Configuration loading mechanism
When the framework starts, config-loader loads configuration files and merges them deeply in the following order:
Runtime merging lets later layers declare only the fields they override. TypeScript intentionally distinguishes the project base from those later patches: default.ts uses VextUserConfig, while environment profile and local config files use VextConfigOverride; createTestApp() applies the same override contract. Bootstrap providers keep their JSON-like Record<string, unknown> patch contract and runtime validation.
When loading TypeScript config sources directly, the framework compiles them without requiring an additional TS loader. Modules execute in the current application process, support top-level await, and their exported provider functions retain access to that process's state. Evaluation of the same TS config module is reused within one process. import.meta.url / filename / dirname identify the module's source location. The framework manages temporary execution files and cleans them after successful or failed loading; externally modified files are preserved and reported as conflicts. Compiled production mode continues to load config artifacts from the selected build output.
Configuration file
Explicit profile selection follows --config <name> → VEXT_CONFIG=<name> → the legacy nonstandard NODE_ENV value (with a warning). Without an explicit selection, vext build defaults to production and vext dev to development. vext start and vext deploy assets first inherit the profile recorded in the selected successful build output, falling back to production only when no profile is recorded.
For example, after npm run build -- --config sg-sit in a clean terminal, npm start inherits sg-sit while still running in production mode. To use another profile, select it explicitly and confirm its configuration file is included in the output. See CLI for commands and build identity checks.
An explicit CLI profile takes priority over VEXT_CONFIG. A nonstandard
NODE_ENV name still has a legacy compatibility route with a warning; use
an explicit profile instead. Standard NODE_ENV does not replace each
command's default mode. A profile name may contain only letters, digits,
underscores, and hyphens; default, local, and bootstrap are reserved.
Do not pass a file path.
Profile names can represent custom deployment environments, for example:
src/config/sg-sit.tssrc/config/us-uat.tssrc/config/us-prod.ts
Pass the profile name at startup:
The second line uses POSIX shell syntax. In PowerShell, set
$env:VEXT_CONFIG = "sg-sit" before the command, or use the cross-shell
--config option. The examples use .ts; the loader also supports .js,
.mjs, and .cjs.
This production start uses default -> sg-sit -> bootstrap provider patch -> CLI override. local.ts is loaded only in development/test runtime modes. Production build and start, for both JS sources and compiled TS, do not implicitly evaluate it. Use an explicit profile or bootstrap provider for deployment overrides; a custom profile does not change the runtime mode.
vext build statically injects process.env.NODE_ENV in user source code as "production", and vext start runs with production runtime mode. Config profile selection is independent and is controlled by --config / VEXT_CONFIG.
Therefore, it is recommended to put the environmental differences into:
src/config/<env>.tssrc/config/bootstrap.ts- Other explicit business environment variables
Instead of relying on the process.env.NODE_ENV conditional branch in the source code after build.
Merge rules
- Plain object fields: deep merge; later layers declare only overrides. Class instances and runtime capabilities are atomic and cannot be recursively patched.
middlewaresarray: smart patch bynameinstead of replacing the whole array.- Other arrays: later layers replace earlier arrays.
- Bootstrap provider patch: participates in merge, validation, and freezing after
local.tsand before the CLI override. - Final result: plain config objects and arrays are deeply frozen. Runtime-capability instances retain their internal mutable state and are not recursively frozen.
TypeScript base and override layers
- Base config (
default.ts): useVextUserConfig. Its top-level fields are optional, but a nested object you provide is not automatically a deep partial. For example, adatabasevalue indefault.tsmust satisfy the completeMonSQLizeDatabaseConfig, including its requiredconfigconnection object. - Override layers: use
VextConfigOverrideindevelopment.ts,production.ts, custom profiles, andlocal.ts. It mirrors runtime deep-merge semantics, so a later layer may patch onlydatabase.findLimitorlogger.levelwhile inheriting the rest from the complete base. - Atomic capabilities: adapters, stores, callbacks, arrays, and registered runtime-capability paths remain complete values rather than being recursively weakened.
Do not split a required base object across files and expect TypeScript to wait for a later merge. A half database in default.ts is invalid even if development.ts supplies its uri; put a complete database connection in the base, then patch only environment differences in later layers. See Database configuration.
Bootstrap Config Provider
If you need to pull the remote configuration (such as Nacos/Configuration Center/Startup Key Distribution) before finalizing the configuration, you can add src/config/bootstrap.ts:
Provider context fields:
dev and build evaluate configuration once from src/config before backend compilation so custom frontend paths are known in advance. Their provider context uses the source configuration directory and isBuilt=false. Backend compilation, frontend builds, and development watching reuse that configuration instead of calling providers again for paths. A compiled start still uses the output configuration directory and isBuilt=true; isBuilt does not mean that the current command is build.
Constraints:
- provider must return plain object patch or
null - patch only supports JSON-like structure; does not support functions, class instances, and adapter factory
- Default priority:
local < provider < CLI - When
requiredis not declared:productiondefaults to fail-fast,development/testdefaults to continue after warning - In Cluster mode, the Master will pass the current round of provider patches to the Worker for reuse to avoid configuration drift in the same startup cycle.
Timeout aborts the signal but cannot forcibly terminate arbitrary user async work. Pass that signal to network operations inside providers. Replace the placeholder remote URL with a real service; it is not required by the opening complete example.
Configuration file format
Export one object per configuration file using export default:
config.session.enabled: true auto-registers Session across production, development, testing, and soft reload. It defaults to false; the built-in memory store is suitable for single-process deployments. Shared deployments should set config.session.store to createCacheSessionStore(cacheLike) or a custom VextSessionStore. Route options can use session: false to opt out or session: true to opt in while the global runtime is disabled. The explicit session() middleware remains available for scoped/manual registration.
config.csrf.enabled: true auto-registers the built-in CSRF middleware after body parsing and plugin global middleware. Keep it disabled and register csrf() manually when you need scoped protection for selected paths.
config.securityHeaders.enabled: true auto-registers low-impact browser security response headers. Use preset: "basic" for the default baseline, and opt into strict or explicit CSP/COEP only after checking your frontend, CDN, iframe, and OAuth popup flows.
Middlewares Patch Strategy
The middlewares array merges by middleware name:
Each layer may declare a middleware name only once. A duplicate in one file
fails startup. A later profile/local layer may patch an earlier declaration.
{ name, enabled: false } keeps the name registered as a no-op and does not
look up or run the original middleware file. The disabled name remains
referenceable, but a route must not pass options to it: the no-op is plain
middleware, not a factory.
Declarations with the same name merge shallowly: a later options value
replaces the whole earlier options object. An empty array does not delete
the inherited whitelist; use enabled: false for an existing item. The
auth, check-role, and rate-limit-api names below require actual
implementations. A whitelist declaration does not create middleware.
Merged result:
Use Adapter
Native Adapter (http.createServer + route-core) is the default. To use
another Adapter, install its optional dependency as described in the
Adapter guide, then merge one of the following four
alternative snippets into default.ts:
When adapter is omitted, Vext uses the Native adapter, which has no third-party HTTP framework dependency. Switch when you need another framework's capabilities or a migration path. Throughput varies by workload, so review the current benchmarks and test your application before deciding.
Frontend configuration (frontend)
frontend controls the built-in browser pipeline. It can be true, false,
or an object. The following combination of optional features assumes pages,
styles, and locale resources from the Frontend guide.
The admin/app/shell page must actually exist. To enable only defaults, use
frontend: true instead of copying the entire snippet.
By default spaFallback.scopes is empty, so unknown HTML paths are not swallowed into the SPA. For mixed SSR + client-router sub-apps, declare each basePath in scopes[]. spaFallback: true is kept only as a compatibility shorthand and is not recommended for enterprise mixed projects.
When frontend.deploy.upload is enabled, vext deploy assets reads the
deploy manifest from the effective selected frontend output (by default
dist/client/deploy-manifest.json) and uploads changed assets by uploadKey
and sha256. The built-in filesystem adapter writes to targetDir for CDN
sync staging; a custom adapter handles a real cloud provider. By default,
upload excludes index.html and **/*.map: Vext still renders HTML on the
server, while source maps can remain on the server for debugging instead of
being published as CDN assets.
This table is a general-configuration overview. For an exact nested field, resolved default, build-output topology, or CDN/upload decision, use Frontend Configuration and the canonical VextFrontendConfig API reference. For creating the app, changing pages, adding components, CSS/JSCSS, assets, API calls, HTML templates, and troubleshooting, see the Frontend guide.
Complete configuration item reference
This section lists common fields and defaults. For cache, fetch, locale, Session/CSRF, and other nested options, use Configuration API as the complete reference. Setting an individual parameter does not necessarily enable its feature.
Basic configuration
In production or containers, host: "0.0.0.0" listens on all IPv4
interfaces, while host: "::" listens on all IPv6 interfaces. For ::,
the ready log also displays http://[::1]:PORT and a bracketed IPv6
network URL. A specific IPv6 host is shown as http://[IPv6]:PORT.
Production or container deployments can use host: "0.0.0.0" for IPv4 all interfaces, or host: "::" for IPv6 all interfaces. When host: "::" is used, the ready log also prints http://[::1]:PORT and bracketed IPv6 Network URLs; explicit IPv6 hosts are printed as http://[IPv6]:PORT.
CORS configuration (cors)
Rate limiting configuration (rateLimit)
When disabled or omitted, Vext does not install the middleware and emits no
rate-limit headers or HTTP 429 responses. app.setRateLimiter() replaces the
implementation only; it does not change this opt-in setting.
keyBy: "user" reads req.user.id and falls back to IP when unavailable.
Global rate limiting runs before ordinary authentication middleware, so it
does not automatically isolate quotas by req.auth. For Redis, route
overrides, and custom implementations, see Rate limiting.
With global rate limiting enabled, a route can set
options.override.rateLimit. The following snippet belongs inside a
defineRoutes callback; handler stands for an existing handler:
Security Headers configuration (securityHeaders)
basic sends X-Content-Type-Options, Referrer-Policy, and X-Frame-Options. strict additionally enables HTTPS-only HSTS, minimal Permissions-Policy, COOP, and CORP; CSP and COEP remain explicit. Routes can opt out with { securityHeaders: false }.
Request ID configuration (requestId)
By default, the framework reads X-Request-Id, calls the generator when
the value is missing or empty, and writes the ID into the response header.
An incoming or generated ID must be a string of 1–512 characters without
control characters or an error is thrown. For an array-valued header, only
the first value is used. This correlates requests; it is not automatically
a distributed tracing traceId.
Log configuration (logger)
Supported log levels (from low to high): 'trace' → 'debug' → 'info' → 'warn' → 'error' → 'fatal' → 'silent'
VextJS has a built-in logger kernel with zero runtime dependency, and the pretty mode uses the built-in formatter to output readable logs. The default logger supports trace(), getLevel() / setLevel() and exact key/path redaction; see Log Document for complete description.
Graceful shutdown configuration (shutdown)
On SIGTERM / SIGINT, a normal HTTP process enters bounded shutdown: it
stops accepting requests, handles in-flight requests, then runs onClose
hooks in reverse registration order. The entire pipeline shares a deadline.
After it expires, remaining cleanup is invoked without further waiting.
The test helper's close does not call process.exit; an unfinished async
cleanup must not be reported as complete.
HTTP Server Configuration (server)
server controls the inbound Node.js HTTP server layer for the built-in Native, Hono, Fastify, Express, and Koa adapters, including the development server created by vext dev. Unconfigured fields retain the current Node.js defaults.
config.server only affects inbound service requests; the timeout for outbound app.fetch / app.fetch.proxy is still controlled by config.fetch.timeout or options when calling.
Response configuration (response)
The response.hideInternalErrors here is aimed at the 500 path of "unknown exceptions", such as throw new Error("...") directly in the code. If you use app.throw(...) to actively throw 404, 409 and other structured HTTP errors, the framework will still return the status code and message you specify, regardless of this configuration.
Actual output of res.json(data) with wrap: true enabled:
Set wrap: false to turn off wrapping, and res.json(data) will output the original data directly.
Body Parser configuration (bodyParser)
maxBodySize accepts strings such as '1mb' or '500kb', or a number of
bytes. This bounds the entire request and does not rise automatically when
multipart.maxFileSize rises. An Adapter or reverse proxy may have a lower
limit.
Multipart / File upload configuration (multipart)
Access Log Configuration (accessLog)
Built-in multipart parsing is memory-only and does not write files to disk.
Ordinary multipart text fields do not automatically appear in req.body,
and a route override cannot rescue a file rejected by the earlier global
limit. See File uploads for a complete example and error
checks.
When enabled, requests are logged at completion according to log level and path filters. This is an illustrative pretty-mode line:
OpenAPI configuration (openapi)
openapi.docs.access.cacheKey is not supported in this release and is rejected by config validation. Add a resolver directly; a future docs caching layer should define its own explicit cache contract.
For fixed local or deployed API targets, configure openapi.servers[].url as the complete base URL including its port, for example http://127.0.0.1:3000. Reserve openapi.servers[].variables for truly variable URL segments such as environment, region, tenant, or API version. openapi.docs.tryItOut.defaultServer selects the initial Try it out server, and openapi.docs.tryItOut.customServer allows a temporary browser-side override without changing project config.
Database configuration (database)
Providing a nonempty database object activates Vext's built-in MonSQLize
lifecycle. There is no database.enabled off switch: omission, null, or
an empty object skips it. The repository currently checks MonSQLize 3.3.0;
this does not pin the Vext installation version. The lifecycle includes
connection normalization, logger bridging, model loading, raw app.db
mounting, and shutdown cleanup. Use the first-class fields for
those owned concerns. database.monsqlizeOptions is a typed, runtime-validated
escape hatch for the documented advanced allowlist; protected or unknown keys
fail before the upstream constructor runs.
See Database (MonSQLize) for the full allowlist, ownership boundary, raw-instance API, Vector Search, and relation-protected deletion prerequisites.
Request context configuration (requestContext)
Disabling requestContext removes request-context lifecycle capabilities and may reduce their overhead, but the benefit depends on the workload and must be measured in your application. The following features will be disabled:
app.loggerautomatically carriesrequestIdapp.throw()automatically parses the request localeapp.fetchautomatically propagatesrequestId
Consider disabling it only after confirming that these capabilities are unnecessary and application-level measurements show a benefit.
Cluster configuration (cluster)
You can also turn on Cluster mode through the environment variable VEXT_CLUSTER=1 without modifying the configuration file.
See the Cluster guide for CPU detection, proxy/NAT limits of source-IP affinity, and rolling restart behavior. Changing sticky or workers requires a full Master restart; vext reload does not switch these policies.
Jobs configuration
config.jobs configures application-started cron / interval jobs. Enabled by default, timers register future points only after plugins, services and readiness complete. Active jobs in built-in Cluster require Redis. Test helpers use supplied definitions without automatically scheduling files. Docs has a separate Job source directory configuration; verify it when customizing jobs.dir.
Dev mode configuration (dev)
dev configuration items are only read in vext dev development mode, production mode (vext start) automatically ignores all fields.
The Dev error overlay is based on Accept content negotiation, not the HTTP method:
Accept: text/html(Browser address bar GET, HTML form POST) → Return to HTML error pageAccept: application/json(frontend fetch / axios / curl) -> always returns JSON.
Console logging is not affected by overlay - logging configured with logErrors behaves exactly the same whether the response returns HTML or JSON.
Middleware whitelist (middlewares)
Only middleware declared in the whitelist can be referenced in the route's options.middlewares.
Access configuration in code
Routing
In service
In plug-in
Plain objects and arrays in app.config are deeply frozen after loading.
Writing a frozen property in strict ESM code throws a TypeError.
Explicit non-plain class instances are not recursively frozen, so this
mechanism does not freeze a Redis client's internal state.
Custom configuration fields
The VextConfig interface allows extending custom fields. Plug-ins and business code can add arbitrary fields in the configuration:
Use with declare module to get type hints:
Environment variables
In addition to configuration files, some settings can also be controlled through environment variables:
VextJS does not automatically parse .env files. A value visible through
process.env must already have been injected by the OS, shell, process manager,
container/CI platform, secret manager, or a loader explicitly owned by the
application. Use --config or VEXT_CONFIG to select a Vext config profile;
an .env file is not another built-in Vext profile layer.
Deployment values can come from project-selected config files, platform injection, or a provider. Vext does not require one universal source. For example:
- Read an environment variable with
process.env.DB_PASSWORD. - Use
local.ts(listed in.gitignore) for local development secrets.
Production mode does not load local.ts; supply production values through
the selected profile, provider, or explicit environment inputs.
Configuration verification
config-loader checks configuration after merging. These are common checks,
not an exhaustive list of all fields and business values:
-
portmust be a positive integer from 1 to 65535. -
adaptermust be a known built-in identifier or a valid adapter object/function -
Each element in the
middlewaresarray must be a string or a{ name: string }object -
rateLimit.maxandrateLimit.windowcurrently check for number type and a value of at least 1; the checker does not fully enforce finiteness or integer values. Applications should use finite positive numbers, an integermax, and seconds forwindow. -
logger.levelmust be a legal log level -
logger.redactKeys/logger.redactPathsmust be a string array,logger.redactValuemust be a string -
shutdown.timeoutmust be a non-negative number (unit: seconds) -
server.requestTimeout,server.headersTimeout,server.keepAliveTimeout,server.socketTimeoutmust be non-negative finite numbers (unit: milliseconds) -
server.maxHeaderSize,server.connectionsCheckingIntervalmust be positive integers,server.maxRequestsPerSocketmust be non-negative integers -
cluster.workersmust be a positive integer or'auto'/'auto-1'
Existing validators report errors during startup. Applications must still check custom fields, external service availability, and business constraints outside those validators. Successful config loading alone does not prove the application works.
Troubleshooting and verification
Next step
- Understand the detailed configuration and switching methods of Adapter Architecture
- Learn how to configure whitelist in Middleware
- See OpenAPI Documentation for advanced configuration
- Explore configuration options for Cluster Multiprocess