Microservices Runtime Guide
This page documents the developer-facing runtime model for ZinTrust microservices.
Use it when you need to:
- understand which files ZinTrust reads at boot
- mount service routes into a monolith runtime
- run a service as a standalone entrypoint
- add service-local config overrides without forking the whole project config
Terminology
This guide uses the following terms deliberately:
- Node runtime: a normal long-lived server process started with ZinTrust in Node.js.
- Cloudflare Worker / serverless runtime: a serverless request runtime started through Wrangler or deployed to Cloudflare Workers.
- Serverless runtime: a general category that includes platforms such as Cloudflare Workers and AWS Lambda.
- ZinTrust worker: a background job worker from the ZinTrust workers system, not the same thing as a Cloudflare Worker.
When this guide says src/zintrust.runtime.wg.ts, it means the runtime module used for the Cloudflare Worker / serverless runtime, not a ZinTrust background worker.
Runtime Model
ZinTrust supports two execution styles from the same repository:
- monolith mode, where the root runtime mounts selected services
- standalone mode, where one service boots with its own runtime context
The runtime source of truth is a generated manifest, not ad hoc filesystem discovery.
Files You Work With
The current runtime contract uses these files:
src/bootstrap/service-manifest.ts
src/zintrust.runtime.ts
src/zintrust.runtime.wg.ts
src/services/<domain>/<name>/routes/api.ts
src/services/<domain>/<name>/src/index.ts
src/services/<domain>/<name>/wrangler.jsoncWhat each file does:
src/bootstrap/service-manifest.tslists services that can be mounted by the root runtime.src/zintrust.runtime.tsexposes the runtime module for Node.src/zintrust.runtime.wg.tsexposes the runtime module for the Cloudflare Worker / serverless runtime, for example when running with Wrangler or deploying to Cloudflare Workers.routes/api.tsis the generated route module for a service.src/index.tsis the standalone service entrypoint.wrangler.jsoncis the service-local Cloudflare Worker config for that microservice.
Service Identity
Every service has a canonical ID in domain/name form.
Examples:
ecommerce/usersecommerce/ordersbilling/payments
ZinTrust normalizes internal lookups to this format. Bare names can still work when they are unambiguous, but developer-facing config and diagnostics should prefer the canonical ID.
Service Manifest
The root runtime reads service definitions from src/bootstrap/service-manifest.ts.
Example:
import type { ServiceManifestEntry } from '@zintrust/core';
export const serviceManifest: ReadonlyArray<ServiceManifestEntry> = [
{
id: 'ecommerce/users',
domain: 'ecommerce',
name: 'users',
prefix: 'ecommerce/users',
loadEnv: false,
port: 3001,
monolithEnabled: true,
loadRoutes: async () =>
import('../services/ecommerce/users/routes/api.ts').catch(
() => import('../services/ecommerce/users/routes/api.js')
),
},
];
export default serviceManifest;Why loadRoutes() is used:
- it keeps route imports explicit
- it works cleanly with Node and Cloudflare Worker bundling
- it avoids relying on string-based runtime module resolution
- it lets the built CLI load source
.tsroutes in consumer apps and fall back to built.jsroutes after compilation
The generated src/zintrust.runtime.ts and src/zintrust.runtime.wg.ts files follow the same pattern for src/bootstrap/service-manifest.ts, so the runtime metadata remains loadable both from source and from build output.
Monolith mounting also uses the manifest prefix:
prefixaffects monolith route mounting onlyloadEnvaffects monolith service env preloading only- standalone service boot keeps the service routes unchanged
- if
prefixis omitted, ZinTrust defaults it todomain/name - developers can override
prefixto any mount path they want
For env loading:
loadEnvdefaults totrue- when
loadEnvistrue, monolith startup preloads the service-local.env*layer before mounting that service - when
loadEnvisfalse, the service still mounts in monolith mode, but ZinTrust skips service-local.env*loading for that manifest entry - scaffolded service manifest entries default to
loadEnv: falseso mounted services do not participate in root/global env merging unless you opt in explicitly
Monolith Route Mounting
When the root runtime boots, ZinTrust loads src/zintrust.runtime.ts, reads the manifest, and mounts route modules whose entries have monolithEnabled !== false.
In Node monolith mode, ZinTrust also preloads each service-local .env* layer by default before mounting that service. Developers can disable that per service with loadEnv: false in the manifest entry.
Generated services standardize on:
src/services/<domain>/<name>/routes/api.tsThat route module should export registerRoutes(router).
Standalone Service Boot
Generated standalone service entrypoints now delegate standalone boot ownership to a core start helper.
The framework-owned standalone boot step is used for:
- identifying the active service at runtime
- resolving service-local config overrides
- starting the Node runtime when the entrypoint is executed directly
- keeping standalone boot aligned with the same runtime primitives used by the root application
For a scaffolded microservice, this entrypoint lives in the microservice root at:
src/services/<domain>/<name>/src/index.tsIt is not the root project entrypoint at src/index.ts.
The generated microservice entrypoint looks like this in principle:
import { bootStandaloneService } from '@zintrust/core/start';
await bootStandaloneService(import.meta.url, {
id: 'ecommerce/users',
domain: 'ecommerce',
name: 'users',
configRoot: 'src/services/ecommerce/users/config',
});
export { default } from '@zintrust/core/start';The important point is that the microservice entrypoint no longer owns the raw runtime setup itself. It delegates that work to core.
Example:
- standalone service route module defines
GET / - standalone service responds at
/ - monolith mounts the same route at
/<prefix>
With the default generated manifest entry for ecommerce/users, that means:
- standalone:
/ - monolith:
/ecommerce/users
If a service route does not appear in monolith mode, check these first:
- the service is listed in
src/bootstrap/service-manifest.ts monolithEnabledis notfalseloadRoutes()resolves the service route module successfully- the root runtime can load
src/zintrust.runtime.ts
Env Handling For Standalone Services
When you start a generated service from its own folder, for example:
cd src/services/ecommerce/users
zin sZinTrust treats env in two layers:
- the project root env files are loaded first
- the service directory env files are loaded after that
If both layers define the same key, the microservice layer wins.
For a service at:
src/services/ecommerce/usersthe effective lookup paths are:
<project-root>/.env<project-root>/.env.local<project-root>/.env.<mode>when the mode is notproduction<project-root>/.env.<mode>.local<project-root>/src/services/ecommerce/users/.env<project-root>/src/services/ecommerce/users/.env.local<project-root>/src/services/ecommerce/users/.env.<mode>when the mode is notproduction<project-root>/src/services/ecommerce/users/.env.<mode>.local
Example:
/workspace/my-zintrust-app/.env
/workspace/my-zintrust-app/src/services/ecommerce/users/.envIf both files define the same key, the service-local value wins for a service-directory start.
This gives you the usual shared-app defaults from the root project and lets the microservice override only the values it needs locally.
bootStandaloneService() now exposes explicit env controls when you want to override the default directory inference:
import { bootStandaloneService } from '@zintrust/core/start';
await bootStandaloneService(import.meta.url, {
id: 'ecommerce/users',
domain: 'ecommerce',
name: 'users',
configRoot: 'src/services/ecommerce/users/config',
rootEnv: true,
envPath: 'config/env/microservices/users/.env.local',
});Rules:
rootEnvdefaults totrue- when
rootEnvistrueor omitted, ZinTrust loads the root project env first - when
rootEnvisfalse, ZinTrust skips the root project env layer - when
envPathis provided, ZinTrust uses that explicit env directory or.envfile instead of inferring the microservice env directory - relative
envPathvalues are resolved from the project root, while absolute paths are used as-is
If envPath is omitted, ZinTrust falls back to the inferred service env directory.
The CLI mirrors this with explicit flags:
zin s --env-path config/env/microservices/users/.env.localzin s --no-root-env
Runtime file resolution still uses ZINTRUST_PROJECT_ROOT, so these project-owned files are resolved from the application root even when the command is launched inside a service directory:
src/zintrust.runtime.tssrc/zintrust.runtime.wg.ts- root
config/*.ts - other project-relative runtime loaders
Practical rule for developers:
- put shared defaults in the root
.env - put service-specific overrides in the service directory
.env - use
envPathwhen you want the microservice env source to be explicit instead of inferred - set
rootEnv: falseonly when the service must not inherit root env defaults - use service-local
config/*.tsonly when you need code-level config overrides, not plain env overrides
If zin s fails with Error: 'tsx' not found on PATH., install tsx in the project with npm install -D tsx.
If you need a machine-wide fallback for ad hoc development, npm install -g tsx also works, but the project-local dependency is the safer default.
Layered Config Overrides
Standalone services can override selected config modules without duplicating the whole root config tree.
Resolution order is:
- root config override in
config/<name>.ts - service-local override in
src/services/<domain>/<name>/config/<name>.ts - service-local values override root values when both exist
Example layout:
config/cache.ts
src/services/ecommerce/users/config/cache.tsThis lets a service start with shared project defaults and only add local overrides where needed.
Supported today:
- Node runtime supports merged root plus service-local startup config overrides.
- Cloudflare Worker / serverless runtime also supports merged root plus service-local startup config overrides for scaffolded standalone services.
Cloudflare Worker / Serverless Runtime
The Cloudflare Worker / serverless runtime uses the static runtime hook and manifest instead of runtime filesystem discovery.
Today that means:
src/zintrust.runtime.wg.tsis the Cloudflare Worker / serverless runtime entry module- the generated manifest remains the source of truth for service mounting
- each scaffolded service gets its own
wrangler.jsonc - aliases that belong to the root application still point back to the root project
Service-local Wrangler layout
Each scaffolded service now gets:
src/services/<domain>/<name>/wrangler.jsoncThat file keeps service-owned paths local, including:
main@routes/api.ts@service-runtime-config/*aliases for optional service-local startup config modules- Cloudflare Worker / serverless vars such as
SERVICE_NAME,SERVICE_DOMAIN, andSERVICE_PORT
Aliases that belong to the root application still resolve to the root project, including:
../zintrust.runtime.wg.js../zintrust.plugins.wg.js@runtime-config/*
This keeps the service Cloudflare Worker / serverless config small while ensuring shared runtime and config modules still come from the root app.
Scaffolding Behavior
When you scaffold a service, ZinTrust now:
- creates or updates
src/bootstrap/service-manifest.ts - creates
src/zintrust.runtime.tsandsrc/zintrust.runtime.wg.tsif they do not exist - generates
routes/api.tsfor the service - generates a service-local
wrangler.jsonc - generates a standalone entrypoint that delegates service runtime setup to core
Recommended Developer Workflow
For most teams, the intended flow is:
- scaffold a service
- implement routes in
routes/api.ts - keep the service listed in
src/bootstrap/service-manifest.tswhen it should mount in monolith mode - add service-local override files only for config that actually needs to diverge
Summary
The runtime contract is meant to stay simple for developers:
- manifest for service registration
- runtime hook for boot-time integration
domain/namefor service identityroutes/api.tsfor service-owned routes- optional service-local config overrides when standalone behavior needs to diverge between Node runtime and Cloudflare Worker / serverless runtime