Skip to main content
Wildo.ai Coming soon

Databases, storage & queues

Configure the backing services your application uses for data, files and background processing.

Databases · object storage · queues · readiness · telemetry

> From environment choices to service connections > From runtime responsibilities to scoped access > From operating signals to useful answers

An application needs more than its own code: somewhere to store records and files, services that carry background work, and signals that explain how it is running.

Wildo brings these dependencies into a shared environment configuration. Choose self-hosted or managed services, derive the connections each runtime needs, and inspect the resulting operation. You retain control of hosting, capacity and operating policy.

Environment choices, scoped access and operating signals connect an application to its backing services.

Make the environment part of the application

Choose services together

Describe databases, queues and storage in one environment. Deployment files and runtime connections follow those choices, so local operation and remote deployment share an explicit starting point.

Connect the right work to the right access

Runtime declarations guide service connections and network membership. Shared secret handling supplies process configuration while keeping operator credentials out of tracked application files.

Understand what is running

Readiness reports failing dependency checks; logs and traces help explain activity. Credential handovers, shutdown and request limits give operators mechanisms for handling change and load.

Example: Run a workflow with records, files and background work

An application stores customer records, accepts attachments and processes queued jobs. Its environment describes the database, storage and broker. A background runtime receives access for its declared work, while readiness and telemetry help the operator investigate a delayed job or unavailable service.

For engineers

Separate the database choice from the service supplying it

The platform’s own store, the application’s selected database engine and the available backing services are separate declarations. This selected excerpt from Wonder Todos’ local infrastructure configuration keeps that distinction visible. Other services, logging and application-specific settings are omitted; the code is an excerpt, not a replacement configuration.

import { defineInfraEnvConfig, WildoEnvironment, WildoDeploymentRuntime,
  BackingServiceSource, DatabaseEngine } from '@wildo-ai/platform-config-lib';

export default defineInfraEnvConfig({
  environment: WildoEnvironment.LOCAL,
  runtime: WildoDeploymentRuntime.DOCKER_COMPOSE,
  publicDomain: 'localhost',
  database: { engine: DatabaseEngine.POSTGRESQL, source: BackingServiceSource.SELF },
  applicationDatabase: { engine: DatabaseEngine.POSTGRESQL },
  backingServices: {
    postgresql: {
      source: BackingServiceSource.SELF,
      host: 'localhost',
      port: 5432,
      database: 'wonder-todos-db',
    },
    s3: {
      source: BackingServiceSource.SELF,
      provider: 'minio',
      endpoint: 'http://localhost:9000',
      bucket: 'wonder-todos-local',
    },
  },
});

database selects the platform store. applicationDatabase selects the engine for application resources. backingServices describes the services used to satisfy those choices. A reachable PostgreSQL address alone does not select it as the application’s database.

Self-hosted selections inform generated service definitions. Managed selections describe external dependencies and their connection requirements. Changing the deployment runtime does not turn a service choice into data migration; moving records, choosing capacity and validating the destination remain deliberate operations.

Keep authored inputs and generated outputs distinct

LayerWhat belongs hereHow it is used
Application configurationRuntime declarations and their resource/platform accessDescribes the work and authority each process needs
Environment configurationDeployment runtime, service choices, endpoints and observability postureSupplies the environment-specific projection
Secret materialOperator credentials and framework-managed keysSupplies authorized connection material without tracking its values
Generated manifests and environment filesService definitions, network membership and process settingsConsumed by deployment and runtime startup
Operating evidenceReadiness responses, logs, traces and delivery observationsShows what the configured environment is actually doing

Synchronize configuration after changing its authored inputs. Inspect the generated service definitions and process environment together. Runtime access resolution and network membership use the same access decision, but operation-level resource permissions remain their own boundary.

The detailed credential guides explain where material is minted, projected and rotated. Do not turn a generated .env into a second configuration source; regeneration must be able to reproduce the intended setup.

Check the projection, then observe the process

For example, after changing an application’s registered production Kubernetes environment:

wildo config sync --env=production --domain config
wildo config validate --env=production --domain manifest --deployment-artifacts

Confirm synchronization actually produced the deployment artifacts. A skip message can mean the platform is unavailable or the application credential was refused; restore platform access and application registration before retrying.

Inspect the resulting service definitions, runtime mounts and connection names. Manifest validation renders infrastructure and platform manifests into a temporary directory. The --deployment-artifacts flag also checks the application YAML files saved by synchronization. These checks inspect structure; they do not apply the deployment or test credentials against the cluster. The flag requires a registered Kubernetes environment and generated application files; it is not a Compose validation command. Use the normal deployment workflow to deliver the artifacts and roll out consumers.

Then query the actual service’s /health/ready endpoint. Read its status, component results and failing names together. A generated manifest, a started process and a successful dependency check are three different observations; the configuration-to-operation chain needs all the relevant ones.

Configure observation independently of console presentation

This second selected fragment from the same local configuration enables a local telemetry receiver while retaining machine-readable console output:

logging: {
  level: 'debug',
  formattedConsoleErrors: false,
},
observability: {
  enabled: true,
  endpoint: 'http://localhost:4318',
},

The environment projection supplies telemetry settings and the launcher preloads instrumentation before application modules. The receiver must accept those exports. A log correlation identifier follows an available request frame; telemetry traces carry their own related identity. Browser analytics and model-call observation have separate activation and content policies.

Connect signals to operating decisions

Use /health/ready to inspect evaluated dependency status before routing traffic. The built-in readiness checks cover configuration, conditional MongoDB and storage, plus a registered contributor. PostgreSQL requires an added contributor check if it must govern traffic eligibility; its startup schema check is not continuing readiness. A process answering liveness does not establish dependency availability. Readiness exposes the condition; the deployment’s traffic and recovery policy decides the response.

For replacement processes, allow time for active work to drain before termination. For API-key or OAuth client-secret rotation, give consumers the new material within the explicitly chosen overlap window. Framework-managed platform secrets instead require coordinated reprovisioning and delivery to their consumers. These are different deadlines serving different lifecycles. Request-frequency and page-size limits likewise bound different costs; choose them around the operation, not as substitutes for query design.

The platform supplies these mechanisms across the environment. The operator still owns service availability, backups, capacity, recovery and the collection policies under which operating evidence is retained.

Choose the services your application runs on

An environment brings together the runtime, service connections and public addresses an application needs. Wildo uses that shared description to prepare deployment artifacts and the configuration each process consumes.

Choose where services run and how the environment is operated. The same model supports local development and deployment preparation, with explicit commands for inspecting, starting and rebuilding local state.

Environment choices connect an application to its database, queue and file services.

From environment choices to a working stack

Describe services together

Keep database, queue and storage choices in one typed definition, with fields that reflect each service’s role.

Carry choices into deployment

Generate connected workloads, addresses and storage inputs for the selected runtime instead of maintaining each file independently.

Make local operation understandable

Start dependencies in order, inspect the running stack and choose deliberately between preserving data and rebuilding from empty.

Example: Run an application with records and attachments

An environment selects PostgreSQL for application records, RabbitMQ for background work and S3-compatible storage for files. Wildo derives their deployment and connection inputs; the developer starts the local session and uses diagnostics to investigate a service that cannot be reached.

For engineers

Separate the decisions that happen to share a database

The environment owns the platform database selection, the application’s default database selection and the backing-service connection. This excerpt follows the local Wonder Todos declaration; its other services and policies are omitted.

database: { engine: DatabaseEngine.POSTGRESQL, source: BackingServiceSource.SELF },
applicationDatabase: { engine: DatabaseEngine.POSTGRESQL },
backingServices: {
  postgresql: {
    source: BackingServiceSource.SELF,
    host: 'localhost', port: 5432,
    database: 'wonder-todos-db',
  },
},

These fields belong inside defineInfraEnvConfig; import the helper and enums from @wildo-ai/platform-config-lib. The server declaration supplies infrastructure. The selections tell the platform and application which persistence engine to use. Changing the selection does not migrate existing data.

Follow the complete configuration chain

StageWhat to inspect
Authored environmentRuntime, hosting choices, addresses and service-specific fields
Provider resolutionWhich supported provider actually resolved for each service
Generated artifactsWorkloads, mounts, routes and per-process configuration
StartupReadiness of dependencies and registration before application launch
DiagnosisHost prerequisites, service connections and process facts

Both Compose and Kubernetes use the shared per-kind self-hosting policy. Databases also consider the platform selection. An omitted cache, broker or object store retains the baseline local workload, while explicit managed selection suppresses it. Scanner and audit storage require an explicit self-hosted declaration. Check provider resolution independently: a managed declaration can suppress a local workload without naming a supported managed provider.

Check configuration before applying it

From a configured application workspace with a registered production Kubernetes environment:

wildo config sync --env=production --domain config
wildo config validate --env=production --domain manifest --deployment-artifacts

Confirm synchronization actually produced the deployment artifacts. A skip message can mean the platform is unavailable or the application credential was refused; restore platform access and application registration before retrying.

The manifest check renders infrastructure/platform Kubernetes artifacts in a temporary directory. --deployment-artifacts also checks the saved application YAML and refuses missing output. Neither check changes those manifests or contacts the cluster. Confirm their intended services and mounts, then use the deployment workflow to apply them. For local source/runtime disagreements, wildo local doctor also reports derived-artifact currency before you diagnose application logic.

Choose the local lifecycle action by its effect

Use wildo local doctor for host checks and Compose service diagnostics, wildo local init to reconcile initialization while retaining service data, and wildo local dev to run a local Compose development session. Kubernetes orchestration is not handled by local dev, and doctor does not validate cluster health. For framework developers, local reinit deliberately wipes and initializes again; local reset leaves the environment stopped and empty. Neither is backup restoration.

Remote operation follows deployment and cluster procedures. Generated manifests express the intended inputs; DNS, credentials, service availability, storage protection and successful deployment still need operating evidence from that environment.

Describe and place services

Describe each environment once Mechanism

An environment describes where an application runs and which services it uses. Wildo turns that definition into the configuration its processes and deployment tools need, reducing separate files to keep aligned.

Choose the runtime, public addresses and service connections in a typed file. Keep credentials in the environment’s secret store; synchronize the derived files when the definition changes.

Example: Use the same database engine for different responsibilities

A local application can use PostgreSQL for its business records while the platform uses PostgreSQL for registrations and operating state. These remain separate selections, even when both connect to the same server.

An environment declaration brings service and runtime choices together.
For engineers
Give the environment a canonical home

Use defineInfraEnvConfig in the filename the loader discovers:

EnvironmentAuthored file
Localinfrastructure/local/wildo.infra.local.config.ts
Any non-local environmentinfrastructure/<environment>/wildo.infra.remote.config.ts
Example custom environmentinfrastructure/review-team/wildo.infra.remote.config.ts

The directory selects the environment; a custom name does not change the remote filename. Environment identifiers accept lowercase alphanumeric segments separated by hyphens, so names such as review-team are possible; the common local, staging and production enum values are conveniences, not the complete vocabulary. Follow the existing project’s discovered environment-file layout when adding another environment.

Separate the service from the data it serves

This excerpt follows Wonder Todos’ local declaration. Its other backing services and policy blocks are omitted; it is the database portion of an environment, not a complete stack recipe.

import {
  defineInfraEnvConfig, WildoEnvironment, WildoDeploymentRuntime,
  BackingServiceSource, DatabaseEngine,
} from '@wildo-ai/platform-config-lib';

export default defineInfraEnvConfig({
  environment: WildoEnvironment.LOCAL,
  runtime: WildoDeploymentRuntime.DOCKER_COMPOSE,
  publicDomain: 'localhost',
  database: { engine: DatabaseEngine.POSTGRESQL, source: BackingServiceSource.SELF },
  applicationDatabase: { engine: DatabaseEngine.POSTGRESQL },
  backingServices: {
    postgresql: {
      source: BackingServiceSource.SELF,
      host: 'localhost', port: 5432,
      database: 'wonder-todos-db',
    },
  },
});

database selects the platform’s persistence engine. applicationDatabase selects the application’s default persistence engine. backingServices.postgresql describes the supplied connection. Naming a server is not itself a request to move existing business data to it.

Ship the PostgreSQL schema with the connection change

Selecting PostgreSQL supplies a default engine and connection; it does not create the application’s tables or transfer existing records. Resource and module schema changes need reviewed migrations and an updated accepted schema baseline, shipped together with the release.

For non-platform applications, startup applies shipped PostgreSQL migrations before checking the physical tables against the resource plan. Platform services run that work in their own startup phase. An absent or incompatible table can therefore stop startup even when the connection is valid.

Follow database schema planning and migration for the workflow. The application’s application-database-migrations guidance covers migration authoring and baseline review; moving existing business data between stores is separate work.

Let each field keep its meaning
DeclarationWhat it identifies
PostgreSQL or MongoDB databaseA database name
Redis databaseAn integer logical index from 0 through 15
RabbitMQ vhostA broker namespace
S3 endpoint and bucketAn object-storage address and bucket

The per-kind schemas reject unrelated fields and incorrect types. defineInfraEnvConfig validates the authored shape; a valid shape still needs usable service addresses and credentials.

Regenerate at the configuration boundary

From a configured application workspace, wildo config sync projects the selected environment into runtime and deployment artifacts. Change the authored file, rather than editing generated connection values. Restart affected processes or apply the deployment through its normal lane to make the new configuration active.

For remote public services, serviceHosts and publicDomain feed the shared hostname resolver used by runtime addresses and ingress routes. Conflicting resolved hostnames are rejected. The descriptor expresses intended configuration; it is not a continuously running cluster reconciler.

Check the generated configuration before deployment

Run these from the configured application workspace with a registered production Kubernetes environment; select the environment whose application artifacts you intend to check:

wildo config sync --env=production --domain config
wildo config validate --env=production --domain manifest --deployment-artifacts

Confirm synchronization actually produced the deployment artifacts. A skip message can mean the platform is unavailable or the application credential was refused; restore platform access and application registration before retrying.

The manifest check renders infrastructure/platform Kubernetes files into a temporary directory. --deployment-artifacts also checks the saved application YAML and refuses missing or empty output. Both parse YAML and check required document fields. It leaves the persisted manifests intact and does not contact a cluster. Inspect the generated service addresses and mounts before applying them; this check establishes render consistency, not cluster admission or working credentials.

Bring the supporting services together Mechanism

Application behavior depends on services around it: a database for records, a cache for shared state, a broker for background work and object storage for files. Wildo brings their declarations, connection details and deployment inputs into one environment model.

Select the services your environment needs. Their templates carry operational details such as storage mounts and health checks, while you own the hosting and operating policy.

Example: Support records, background work and attachments

A task application stores records in PostgreSQL, sends work through RabbitMQ and keeps attachments in S3-compatible storage. These are different service roles, described together so their connections can be generated for the processes that use them.

Selected backing-service roles form the environment around an application.
For engineers
Declare a meaningful set

Adapted from the current local environment, this block belongs inside defineInfraEnvConfig. The imports and other environment fields are omitted here; BackingServiceSource comes from @wildo-ai/platform-config-lib.

backingServices: {
  postgresql: {
    source: BackingServiceSource.SELF,
    host: 'localhost', port: 5432, database: 'wonder-todos-db',
  },
  rabbitmq: {
    source: BackingServiceSource.SELF,
    host: 'localhost', port: 5672, vhost: 'wonder-todos',
  },
  s3: {
    source: BackingServiceSource.SELF, provider: 'minio',
    endpoint: 'http://localhost:9000', bucket: 'wonder-todos-local',
  },
}

The database name, broker virtual host and storage bucket identify different namespaces. The backing-service kind maps to an infrastructure capability; the runtime’s grants determine which connection material it receives. A declaration and permission to use that service are connected decisions, not the same decision.

Understand which workloads are emitted

Both Compose and Kubernetes consult selfHostsBackingService. The current omission policy is deliberate:

Service familyLocal-workload selection
MongoDB and PostgreSQLCombines backing-service declarations with the platform database selection
Redis, RabbitMQ and S3Explicit managed selection removes the local workload; omission retains the baseline service
ClamAV and immudbRequires explicit self-hosted selection
BackupNo workload is emitted

MongoDB’s search process follows its database’s selection. Document rendering and telemetry are not additional backing-service kinds: their configuration has its own provider/runtime path. Do not add invented gotenberg or metrics keys to this block.

Follow the generated result

The Compose generator passes inclusion flags, host ports, internal ports, networks and data paths into the platform template. Stateful service directories follow the same selection. Kubernetes derives its workload list from the same self-hosting policy, then applies its own manifest and readiness mechanics.

Generation does not establish that credentials work or that a service has started. Inspect the resolved provider, generated workload and runtime diagnosis as separate steps. The backup declaration has no concrete provider; it must not be interpreted as a backup or restore service.

Choose where each service runs Mechanism

Your database does not have to be hosted in the same way as your queue or file storage. Wildo separates a service’s role from its hosting model, so supported local and managed choices can be made per environment.

The application uses the resolved connection. You choose the provider, supply its operating authority and plan any data migration when changing services.

Example: Use a managed database with a self-hosted broker

An environment can select MongoDB Atlas for records and retain RabbitMQ inside its own stack. Each keeps its own configuration and credential path; the application does not need vendor-specific hosting decisions scattered through its business operations.

A database shown with two hosting choices: your own stack or a managed service.
For engineers
Keep hosting and brand separate

This is a configuration fragment using the current schema, adapted from the production environment. Add it to the selected environment’s backingServices; account details and secrets remain outside this excerpt.

backingServices: {
  mongodb: {
    source: BackingServiceSource.MANAGED,
    provider: 'atlas',
    database: 'application-records',
  },
  rabbitmq: {
    source: BackingServiceSource.SELF,
    vhost: 'application-work',
  },
}

source selects the hosting model. provider selects a brand recognized by the provider resolver. The declaration does not create a vendor account or migrate data. Provider provisioning uses the operator authority available to the selected environment, and runtime delivery uses the generated application credentials.

Check the actual provider resolution
KindCurrent managed resolution
PostgreSQLNamed RDS, Cloud SQL, Azure, Supabase, Neon or Render entries; generic managed fallback
MongoDBatlas
S3Recognized minio and aws brands resolve first, independent of source; otherwise source selects AWS for managed and MinIO for self-hosted
Redis, RabbitMQ, ClamAV, immudbNo managed provider in this resolver
BackupNo concrete provider

PostgreSQL’s unrecognized brand falls back to its generic managed entry. S3 dispatches recognized brands first and otherwise falls back by source. MongoDB has no generic managed fallback. Inspect the resolved infrastructure section of wildo-saas.lock.json after synchronization; a syntactically valid choice alone is not evidence of a supported provider.

Check what can actually execute

A resolved provider name and a lockfile entry describe a selection; neither proves that a provisioning implementation exists. The lockfile can record the authored selection even when no provider module contributes an executable plan.

LayerWhat to verify
Accepted declarationThe service fields and hosting/provider combination pass their schema
Resolved providerThe resolver selects the intended provider enum
Executable moduleA registered module implements the operations needed, such as planning, provisioning or credential minting
Operator-supplied connectionThe selected runtime receives a compatible address and credentials when infrastructure is supplied externally
Runtime authorityThe application’s delivered credential has the required scope and the service is reachable

The built-in provider catalog currently has a self-hosted PostgreSQL module, but no modules for its managed PostgreSQL enum entries. Those names are not a claim that Wildo provisions RDS, Cloud SQL, Neon or the other listed offerings. This does not prevent using an externally supplied PostgreSQL connection; its configuration and operational responsibility remain separate.

Separate provider resolution from workload selection

The local workload policy is shared by Compose and Kubernetes. Explicit managed cache, broker or S3 choices suppress those local workloads, even though a managed cache or broker has no provider in the resolver above. Omission retains their baseline local workloads. This distinction matters: removing a container does not prove a replacement was provisioned.

Before changing a hosting choice, check the provider connection, runtime credential, generated deployment and existing data separately. The common application contract reduces hosting-specific code; it does not remove service compatibility or migration work.

Prepare deployment

Run the stack with Compose Mechanism

Wildo generates a Compose stack from the environment’s service choices. Databases, queues and other supporting processes receive the configuration, mounts and network connections that belong together.

Use the local commands to start and inspect infrastructure. During development, application processes run through the supervised development command; a container deployment uses its generated application stack.

Example: Start the services before the application

A developer brings up the local infrastructure, checks its status, then starts the application development session. The database and broker are ready before application code begins trying to use them.

Environment choices become services in a Docker Compose deployment.
For engineers
Choose the command for the intended result

From an application workspace whose local environment has already been set up:

# Start and inspect the supporting stack.
wildo local up
wildo local status

# Start the supervised application development session.
wildo local dev

local dev includes infrastructure preparation, so a normal working day need not run up separately. Use up when you specifically want the supporting stack. local down stops it; local reset additionally removes local data and asks for confirmation.

Read generated Compose as a projection

The platform template carries backing-service workloads and, for the application-creator lane, platform services. The application template carries application containers and their routing. Framework developers can run platform processes natively, so their generated platform stack is not identical to an installed application’s stack.

Generated detailSource of the decision
Whether a local service is includedShared self-hosting predicate and database selection
Published host portEnvironment override or service default
Container-side port and addressService catalogue and container perspective
Persistent host directoryEnvironment data-path resolver
Runtime network membershipRuntime infrastructure needs

The host-facing connection and the container-internal address need not be identical. A host port override avoids a local collision without changing where the image listens internally.

Preserve startup and storage behavior

The template’s health checks support dependency readiness, with fast startup checks and a quieter steady-state cadence. Generated start_interval requires the Compose plugin version accepted by Wildo’s preflight. Memory limits bound resident services; persistent bind mounts preserve state across container replacement.

MongoDB binds /data/db and /data/configdb individually because a parent mount can be shadowed by the image’s declared child volumes. Do not simplify these generated mounts by hand.

The MongoDB search sidecar illustrates why startup is more than launching containers. Its template waits for the initializer that creates its authentication user:

mongot:
  depends_on:
    mongodb-init:
      condition: service_completed_successfully
  mem_limit: 2048m
  healthcheck:
    interval: 60s
    timeout: 10s
    start_period: 30s
    start_interval: 3s

This is an excerpt of the current template, omitting its image, mounts and probe command. The dependency is successful initialization, not merely a running database. The separate startup cadence checks readiness promptly without repeating the same expensive probe every few seconds throughout the day.

On a server, deployment automation applies the generated artifacts. Local startup is not proof of a server deployment or of Kubernetes equivalence; verify the selected lane’s rendered configuration and operating result.

Describe the stack for a cluster Mechanism

Wildo translates an environment into Kubernetes workloads and their supporting configuration. Application processes, selected backing services, storage and public routes are described together instead of maintained as unrelated manifests.

You choose the cluster and deployment policy. The generated manifests provide deployment inputs; the cluster and its controllers perform the running work.

Example: Give production its own deployment shape

An application can declare a production namespace and several backend replicas while retaining the same service roles used locally. The resulting cluster manifests express that environment’s placement and routing choices.

An environment is projected into Kubernetes deployment resources.
For engineers
Author the cluster-facing fields

This excerpt follows Wonder Todos’ production descriptor, with a neutral domain and names. Backing services and other policy blocks are omitted.

export default defineInfraEnvConfig({
  environment: WildoEnvironment.PRODUCTION,
  locationType: WildoLocationType.REMOTE,
  runtime: WildoDeploymentRuntime.KUBERNETES,
  runtimeEnvironment: RuntimeEnvironment.PRODUCTION,
  publicDomain: 'example.com',
  serviceHosts: { app: 'app', docs: 'docs', website: '' },
  remoteProvider: {
    provider: 'scaleway', registryEndpoint: 'rg.fr-par.scw.cloud/example',
  },
  kubernetes: {
    namespace: 'application-production',
    ingressClass: 'traefik',
    services: {
      backendApi: { replicas: 3 },
      app: { replicas: 3 },
      website: { replicas: 2 },
    },
  },
  deployment: {
    branch: 'main', runtime: WildoDeploymentRuntime.KUBERNETES,
    namespace: 'application-production',
  },
});

The configuration helpers and environment enums come from @wildo-ai/platform-config-lib. Service keys match the application’s declared service keys, such as backendApi, app and website, rather than generated image names. Synchronization carries their replica counts into each deployment; an omitted count defaults to one. An override without a matching emitted application deployment produces a warning, including frontends served through a CDN. Worker replicas remain in the worker declaration. Replica declarations describe desired workload size; they are not proof of availability or spare cluster capacity.

Import RuntimeEnvironment from @wildo-ai/saas-models. Remote generation also needs runtimeEnvironment and the authored remoteProvider.registryEndpoint shown above. That endpoint is the only image registry: synchronization carries it into the workload configuration, and the generated deployment workflow pushes images to the same place. The deployment block has no registry field to keep aligned with it.

Understand the two application boundaries

Locally, the bootstrap creates a Kind cluster, prepares storage, installs the ingress stack, applies manifests and checks readiness. Host storage is mounted into the node. Remotely, generation supplies manifests and the deployment workflow applies them to the selected cluster.

getDeployableInfraServicesForK8s selects local backing workloads through the same self-hosting predicate used by Compose. The readiness loop follows emitted workloads; MongoDB search readiness follows its database initialization. Storage manifests have their own rendering path using the same selection policy.

Keep each process’s configuration explicit

Application deployment templates consume a runtime environment secret and provider-specific secret material. Required environment material prevents a process starting with its essential configuration missing; optional provider material supports runtimes with no such provider declaration. These are generated per-runtime inputs, not an instruction to share one broad secret across every pod.

The backend deployment template makes that distinction directly. This template excerpt is generated structure, not a file to author; the names are supplied by the shared runtime-secret naming functions.

envFrom:
  - secretRef:
      name: {{runtimeEnvSecretName}}
  - secretRef:
      name: {{providersSecretName}}
      optional: true

The missing optional flag on the first reference is significant: losing essential runtime configuration must prevent startup. The second reference is optional because a runtime may declare no external providers.

Cluster storage, ingress, certificates and controller installation remain operational concerns to verify after applying the artifacts. The local Compose telemetry collector does not establish a Kubernetes collector deployment; a cluster environment must have a reachable telemetry destination of its own.

Check the generated configuration before deployment

Run these from the configured application workspace, selecting the environment you intend to change:

wildo config sync --env=production --domain config
wildo config validate --env=production --domain manifest
wildo config validate --env=production --domain manifest --deployment-artifacts

Confirm synchronization actually produced the deployment artifacts. A skip message can mean the platform is unavailable or the application credential was refused; restore platform access and application registration before retrying.

The first validation command checks freshly rendered infrastructure and platform manifests in a temporary directory. Adding --deployment-artifacts also checks the application files produced by synchronization under .wildo-saas/deploy/production/k8s: workloads, ingress, Secrets and every other YAML/YML document in that directory. Missing or empty deployment output fails.

Both checks parse YAML and require resource identity fields. They leave persisted files intact, omit document payloads from diagnostics and do not contact a cluster. Inspect service addresses and mounts before applying the artifacts; Kubernetes admission and working credentials require deployment checks.

Give public services secure addresses Mechanism

A public application needs addresses that reach the right service and certificates that browsers can trust. Wildo derives public routes from the environment’s hostnames and connects the deployed edge to certificate issuance.

Choose the domain and service names, then make DNS and the deployed edge reachable. The configured edge or certificate controller handles issuance; the operator owns the environment in which it runs.

Example: Put the application and documentation on their own addresses

The application uses app.example.com, documentation uses docs.example.com and the website uses example.com. The same hostname declarations guide the ingress and the addresses given to application consumers.

Public hostnames reach their services through ingress and TLS configuration.
For engineers
Declare the names and issuer

This fragment belongs in a remote Kubernetes environment. Provider identifiers and registry settings below are illustrative operator choices; the field shape and service-host pattern follow the production descriptor.

publicDomain: 'example.com',
serviceHosts: {
  app: 'app',
  docs: 'docs',
  website: '',
},
remoteProvider: {
  provider: 'scaleway',
  registryEndpoint: 'rg.fr-par.scw.cloud/example',
  acmeEmail: 'operations@example.com',
  tlsIssuer: WildoTlsIssuer.LETSENCRYPT_STAGING,
},

Import WildoTlsIssuer from @wildo-ai/platform-config-lib. Validate DNS and routing with the staging issuer, then select LETSENCRYPT_PROD for browser-trusted issuance. The staging certificate is intentionally not a production trust result.

Use the TLS owner for the selected runtime

The public hostname inputs are shared; certificate ownership differs:

RuntimeTLS owner and issuance inputsWhere certificate state lives
Remote Docker ComposeTraefik discovers the generated container routers, redirects HTTP to HTTPS and uses Let’s Encrypt HTTP-01 with remoteProvider.acmeEmail/letsencrypt/acme.json in the persistent wildo-letsencrypt volume
Remote Kubernetescert-manager uses the selected ClusterIssuer and its ACME registration email; the ingress requests certificates for its routed hostsKubernetes TLS Secrets, referenced by the ingress
Local Kubernetes edgeSeparate self-signed issuer and local wildcard certificateLocal cluster certificate/Secret resources; workstation trust is configured separately

The remote Compose application generator supplies the same resolved service hosts and ACME email to its Traefik template. Preserve its certificate volume across container recreation and make the HTTP challenge reachable. The Kubernetes tlsIssuer staging/production choice shown above does not configure the Compose resolver.

Follow the generated ingress

Wildo’s Kubernetes workflows install Traefik. kubernetes.ingressClass accepts traefik or omission; other controller values are rejected in authored and saved configuration. Application ingress and HTTP-01 challenge routing use that same supported class.

The shared public-host resolver produces ingress routes and advertised runtime endpoints. It rejects hostname collisions. The application ingress template enumerates those hosts, points each at its own service and adds the configured cert-manager.io/cluster-issuer annotation.

A per-ingress TLS secret can cover the hosts enumerated on that ingress. Do not interpret this as one independently generated certificate for every hostname. Remote issuer templates use HTTP-01, while the retained wildcard alternative is not part of that rendering path.

Keep CDN frontends outside the application edge

For a frontend with enabled: true in the environment’s cdn map, Wildo preserves its advertised public URL but removes its self-hosted deployment target and public route. Kubernetes therefore emits no nginx Deployment/Service or ingress route for that frontend; remote Compose emits no corresponding application container or Traefik router. Both consume the same filtered target list.

The CDN must serve that hostname through its own publishing, origin, DNS and TLS setup. Follow the frontend CDN publishing guide for that separate path.

Distinguish the local cluster edge

The local Kubernetes bootstrap applies the separate self-signed issuer and wildcard certificate for its local domain. It does not request a public certificate through the remote HTTP-01 path. Browser trust in that local authority is a separate machine concern; a successfully applied local certificate is not evidence of remote public issuance. Ordinary host-based local development endpoints remain a different topology from the cluster edge.

Keep controller work and operator work distinct
Wildo’s generated inputsEnvironment requirements
Host-to-service routesDNS reaches the deployed edge
Issuer and TLS annotationsCertificate controller and ingress controller are operating
Named certificate secretSuccessful challenge and certificate issuance
WebSocket affinity annotationsAppropriate application/session behavior behind the edge

The ingress template includes host-header pass-through and sticky-cookie routing. It does not add a general edge rate limiter or security-header policy; those belong to the application or the operator’s chosen edge configuration. Generated YAML alone does not establish successful remote issuance.

Keep local data in a predictable place Mechanism

Containers can be replaced without treating their data as disposable. Wildo gives local stateful services a predictable directory under the application, and connects the generated mounts to that location.

Stopping the stack retains that data. Resetting it is a separate, destructive action that identifies the directories it will remove.

Example: Restart a database without starting from empty

A local database container is recreated after a configuration change. Its data directory remains mounted from the application workspace, so replacing the process does not itself discard the records.

Local services keep persistent data under a shared environment data root.
For engineers
Inspect the expected layout

Local data belongs to the application workspace. Selecting another environment changes the service selection, not the base local_data directory.

The service list is derived for the selected environment. This illustrates the paths for an environment running these services; other selected services have their own directories.

<application>/.wildo-saas/local_data/
  mongodb/
    db/
    configdb/
  mongot/
  postgresql/
  rabbitmq/
  minio/

getDockerComposeLocalDataServices selects stateful services using the same inclusion rules as Compose. EnvironmentPaths.ensureLocalDataServiceDirs creates their host directories before Compose mounts them. MongoDB’s child directories come from INFRA_SERVICE_DATA_SUBDIRS; they must correspond to its actual /data/db and /data/configdb targets.

Understand the local cluster’s relationship

Kind mounts the host data tree into its node and local volumes refer to that mounted storage. This gives the two local runtime lanes a known host location; it is not a promise that switching database versions or runtime layouts requires no compatibility checks. Compose pre-creates individual service paths; local cluster materialization follows its own storage bootstrap.

Choose retention or deletion deliberately
ActionData consequence
Stop the local stackKeeps service data
Recreate a container with the same valid mountReuses the mounted data
wildo local resetStops the environment and removes its selected data directories
wildo local reinitFramework developers only: performs the wipe, then initializes again

The wipe runs after workloads release storage. It targets the selected service directories rather than deleting the parent indiscriminately, and aggregates removal failures instead of reporting success after a partial wipe.

This is local persistence, not backup or replication. An omitted cache, broker or object-store declaration retains its baseline local service; an explicit managed choice suppresses its local directory with its workload. Check the resolved set before any destructive operation.

Work with the local stack

Start development in the right order Mechanism

A local application needs more than a running frontend. Wildo prepares its supporting services and generated configuration before starting the development processes that depend on them.

One session coordinates ownership, startup, logs and shutdown, so failures can be understood in the context of the whole stack.

Example: Resume a working day with one session

A developer using a local Compose environment runs wildo local dev. The command checks prerequisites, prepares infrastructure and registration, then launches the application’s development processes. When the session ends, coordinated shutdown lets the processes finish writing their combined log, with a bounded wait if a process does not close.

A local session starts dependencies, checks readiness and starts the application.
For engineers
Start from a configured local Compose workspace
# Start the selected application's local Compose development session.
wildo local dev

# Inspect it from another terminal, without starting another session.
wildo local dev-status

local dev supports local Compose environments; it refuses Kubernetes environments. The local environment and its authored secrets must already exist. Initial application setup uses the registered setup/initialization journey; this command does not silently replace missing credentials with new values.

Understand the order before diagnosing a failure
StageWhy it precedes the next
Acquire session ownershipAvoid conflicting writers and cleanup against a live owner
Reap stale selected processes and run preconditionsClear abandoned state and identify host blockers
Prepare artifacts and supporting servicesSupply usable infrastructure inputs
Platform readiness and application registrationMaterialize the application’s runtime configuration
Start the application development treeLet processes consume the prepared environment

Framework-development mode can supervise native platform processes, while the application-creator lane uses its platform containers. The application process set follows the selected development scripts; a runtime declaration alone does not prove that a particular worker is launched by those scripts.

Read the cause, not just the signal

createLocalDevShutdownCoordinator distinguishes an external interrupt from an internal bootstrap failure or a managed process exit. It sends termination to managed children, waits for their close/drain promises and then closes the combined output. Each process tree has a 15-second exit wait by default. A timeout is reported before the output is closed, so a process that never closes does not hold shutdown open indefinitely. A printed termination signal is therefore not sufficient evidence that another process killed the session.

The output boundary frames stdout and stderr independently and preserves the machine log while terminal presentation follows the authored logging policy. Use local dev-status to inspect compiler/supervisor and endpoint facts, including when the normal TypeScript CLI layer cannot load. It is an observation surface, not an automatic restart or repair loop.

Attach another application to a shared platform

A second session using the same owned roots is refused. In framework-development mode, another application can attach to a healthy platform already owned by a separate local Compose session. Run this from the second application’s configured workspace, leaving the owning session running:

# Reconcile only this application's registration and runtime configuration.
wildo local init --register-only

# Start this application's processes without starting the native platform again.
wildo local dev --skip-platform

--register-only avoids shared infrastructure initialization. --skip-platform keeps native platform startup and platform-root process cleanup out of the second session; when a live foreign owner is detected, shared container convergence is skipped too. If that owner’s infrastructure is down, the attaching session refuses to start it from the second application.

This is the framework-developer shared-platform path. Application creators use the platform-container lane described above; the native-platform attachment sequence is not a general Kubernetes startup recipe.

Find what is stopping the stack Tool

A startup failure may come from the machine, a service connection or application code. Wildo checks host prerequisites separately from the running stack, helping identify the layer that needs attention.

The report observes and explains. Repairs remain explicit actions, so asking for a diagnosis does not stop processes or delete local state.

Example: Identify a connection problem before changing application code

A database container appears healthy, but the platform administrator credentials no longer match it. The diagnostic checks the service connection as well as container state, making the difference visible.

Preflight checks and diagnostic evidence identify local operating problems.
For engineers
# Report host prerequisites and Compose running-stack checks.
wildo local doctor

# Observe the development processes and their current facts.
wildo local dev-status

Run these from the configured application workspace so they resolve the intended environment. The doctor does not initialize a missing environment on your behalf. Its running-service probes target Compose containers. On Kubernetes it still runs host checks and warns that cluster-specific diagnosis is not covered; a healthy host result does not establish pod or cluster health.

Separate preparation from runtime probes
Check familyWhat it investigates
Host prerequisitesWorking Node/package manager, native toolchain where relevant, Compose compatibility
Generated inputsProvider runtime modules required by the declared runtime hosts
Process leftoversOrphaned development watchers rather than every matching process
Compose runtime checksContainer health, published connectivity and platform administrator authentication
Additional diagnosisEmulated containers, stale credentials and other environment findings

For a local Compose session, local dev calls the shared collectLocalPreconditionChecks before starting its application tree. A failure blocks startup; a warning is printed and may continue. Running-stack probes remain in doctor, because a preflight must not require the stack it is about to start to be healthy already.

Is the running code current?

wildo local doctor also requests the derived-artifact currency sweep and the coding-agent probe. The shared development preflight omits these additional checks unless requested: it must remain suitable for starting the stack, while doctor spends more time diagnosing it.

When source and behavior disagree, read the currency result before changing the implementation. Source files, compiled output, injected package snapshots and an already-running process can represent different revisions. Currency findings are advisory; follow the named artifact and remediation, then confirm the process has loaded the refreshed output. A green compiler result alone does not establish which code a running service uses.

Interpret each result as an action

Example: a MongoDB container is running, but its administrator authentication fails. This is a shortened diagnostic result; the check name, status and authentication detail come from the implemented probe:

{
  "name": "mongo admin auth",
  "status": "fail",
  "detail": "Authentication failed"
}

For this result, the check suggests comparing the container’s MONGO_INITDB_ROOT_PASSWORD with the administrator connection in the apps-manager’s MONGO_CONNECT_URL. Resolve the configuration mismatch before choosing a repair. A suggested reset wipes data; it is not a prerequisite for reading or understanding the report.

The database probes authenticate with platform administrator credentials. Success establishes that administrator connection, not the application’s own credentials or permissions. Listing stale application users is a separate inventory check; it does not authenticate as each user.

The shared diagnostic contract carries the observation and its next step separately. This is the framework result shape used by the checks and their renderers, not application configuration:

export enum LocalPreconditionCheckStatus {
  PASS = 'pass',
  WARN = 'warn',
  FAIL = 'fail',
}

export interface LocalPreconditionCheckResult {
  readonly name: string;
  readonly status: LocalPreconditionCheckStatus;
  readonly detail?: string;
  readonly fix?: string;
}

A failure commits the preflight to blocking. A warning communicates a finding while allowing continuation. The optional fix carries guidance rather than executing it, preserving the separation between observation and repair.

Prefer executed evidence

The toolchain checks execute the relevant binaries; the C++ probe compiles a small modern-language program where supported. This distinguishes a reported version from a usable installation. The network and authentication probes similarly ask more than whether a port was declared in a Compose file.

A result can include a suggested remediation. Apply that action separately after understanding its scope. Orphan detection does not kill the processes it finds, and reporting stale service users does not remove them.

A successful diagnostic establishes only its measured checks. It does not prove the application implements its business rules correctly, nor that an unprobed platform is healthy. Use the per-check status and detail rather than treating the command as a blanket correctness certificate.

Repair or rebuild a local environment Tool

A local environment can become inconsistent across generated files, credentials, service readiness and application registration. Wildo provides an initialization journey that reconciles those layers while preserving backing-service data.

When an empty start is intentional, framework developers can use a separate rebuild command to remove local data before running that journey. Rebuilding is not restoring a backup.

Example: Keep records while repairing configuration

After a framework update, a developer wants current runtime configuration without losing local test records. wildo local init performs the non-destructive initialization path; clearing the databases requires an explicit different command.

Repairing configuration while retaining data is distinct from starting with empty data.
For engineers
CommandWho can use itIntended result
wildo local initApplication creators and framework developersReconcile initialization and preserve backing-service data
wildo local init --register-onlyBoth audiences, with a healthy existing platformRegister this application without shared platform convergence
wildo local reinitFramework developers onlyWipe selected local data and initialize again
wildo local resetApplication creators and framework developersStop and wipe, leaving the environment empty

These commands require an active local environment. Initialization also needs a targeted application root and the installed CLI/framework path. reinit additionally checks framework-developer mode; --force does not bypass that check. Application creators who deliberately need an empty restart use local reset, then invoke local init separately.

Follow a Compose repair with application startup
# Preserve backing-service data while reconciling initialization.
wildo local init

# In a local Compose environment, start application development afterward.
wildo local dev

Initialization converges generated artifacts, backing-service readiness, platform initialization, application registration and runtime environment projections. Its completion includes an executable platform JWT/configuration postcondition. It prepares the infrastructure and control plane; application processes start separately.

The command needs the targeted application root and an existing local environment. Missing or invalid authored secrets must be restored or deliberately prepared through setup; repair must not invent a competing credential set.

Treat an empty rebuild as a different operation

In framework-developer context, local reinit stops the relevant supervised/tracked processes, wipes through the selected runtime lane and delegates back to initialization. Compose stops containers before host directories are removed. The cluster path releases claims and local storage before recreating the environment. Failed removal is reported rather than hidden behind a later bring-up.

In interactive mode, the destructive commands try to list the absolute data paths before asking for confirmation. That preview is best-effort: a path-resolution failure can leave the prompt without a directory list. Confirm the selected application and data scope before proceeding. Non-interactive execution requires explicit destructive opt-in. Do not use that mode merely to bypass a question during diagnosis.

Respect a live shared owner

A full initialization can disturb shared platform processes and generated state, so a live owning session causes a refusal. The --register-only path reconciles this application’s registration and projections against the existing platform instead. These commands are local lifecycle tools; they do not restore remote business data or replace a remote deployment procedure.

Give each process the access its work needs

The backend and separately declared background services each need their own connections and credentials. Wildo carries those declared needs into process configuration and network access, keeping the different parts aligned.

Keep configuration in source control and secret values in the appropriate environment or secret store. The framework handles the generated projections; operators control their distribution and lifecycle.

A separate secret store supplies each runtime with its own credentials and environment.

One declared purpose, consistent access

Provision without accidental rotation

A shared material registry fills missing framework credentials and keeps deliberate rotation separate from ordinary synchronization.

Narrow what each runtime receives

Runtime declarations shape grants, delivered secret material and service reachability, including an explicit empty-access result.

Keep deployment inputs coherent

Per-process environments and CI secret reconstruction derive from their canonical inputs, preserving key material across different readers.

Example: Run a read-only background task

A minion declares read access to todos and no write access. Its scoped credentials and generated environment follow that declaration; it does not receive the backend’s broad credential merely because one of its own grants is absent.

For engineers

Author runtime authority before generating artifacts

A minion is a separately declared background service with tick-based execution. This selected entry belongs inside Wonder Todos’ wildo.saas.config.ts; its independent platform-access settings are omitted:

minions: {
  marketingScrapper: {
    path: './minions/marketing-scrapper',
    resourceAccess: {
      read: ['todos'],
      write: [],
    },
  },
},

Run wildo config sync after editing the declaration, then inspect the minion’s generated environment and network membership. A worker is a separate runtime category; it does not receive the minion’s backing-service set by default. Write permission does not imply read permission. Resource-service access, backing-service credentials and network topology consume the declaration through their own boundaries. Platform-access scopes and internet egress are separate choices; do not treat application-data access as permission for either.

Preserve the chain across provisioning and delivery

StageWhat the framework derives
Managed material registryPresence, provisioning and rotation of framework-owned credential groups
Runtime principalWhich application or background runtime is presenting a credential
Provider provisioningThe scoped database, queue, cache or storage material
Authenticated fetchRemoval of broader material before the principal’s own values are overlaid
Environment generationThe raw values and encoding needed by that process’s reader
Network projectionReachability corresponding to the declared backing-service access

The application backend owns the base application material. Other principals do not inherit that material through a missing section. A short-lived object-storage session is scoped using the platform-issued application identifier; it is a different contract from database ownership.

Keep secret operations explicit

Use synchronization to regenerate derived configuration and provision missing material in file-backed environments. Rotation is a separate choice that must reach every affected signer, verifier or backing service. CI reads persisted secret-store values rather than creating credentials for the lifetime of a runner.

A running process keeps the environment it loaded until restart or rollout. Protect generated secret-bearing artifacts and keep source configuration separate from those outputs. Platform services access application configuration through a signed-token route; application runtimes present their own platform secret. Retrieval of a platform service’s own credentials uses a shared platform-secret boundary; the service-provisioning guide explains that distinction. Endpoint authorization and tenant access remain separate decisions.

Prepare trusted inputs

Fill missing framework credentials without replacing working ones Mechanism

Wildo declares the credential material it manages in one registry. Configuration synchronization can provision missing material while keeping already-provisioned credentials, so an existing environment can acquire what a new framework capability needs.

Example: Provision missing secrets without replacing working ones

An environment gains a new signing-key material during synchronization. The keys already used by its running services remain in place; deliberate rotation follows a separate action.

Existing database and API keys remain in place while a missing framework signing key is provisioned.
For engineers

wildo config sync invokes provisioning for file-backed environments. Each registry material declares its identity keys, generated secret keys, purpose, presence check and provisioning/rotation functions.

The provisioning loop checks each material before and after minting:

Selected source from framework-managed-secrets.ts:

export function provisionFrameworkManagedSecrets(
  secrets: PlatformEnvSecretsDraft,
): FrameworkManagedSecretsProvisioningResult {
  const mintedRefs: FrameworkManagedSecretMaterialRef[] = [];
  const mintedKeys: string[] = [];

  for (const material of FRAMEWORK_MANAGED_SECRET_MATERIALS) {
    if (material.isProvisioned(secrets)) continue;

    material.provision(secrets);

    if (!material.isProvisioned(secrets)) {
      throw new Error(
        `Framework-managed secret material '${material.ref}' did not satisfy its own provisioning check. `
        + 'This is a registry authoring bug in framework-managed-secrets.ts, not an environment problem.',
      );
    }

    mintedRefs.push(material.ref);
    mintedKeys.push(...material.identityKeys, ...material.secretKeys);
  }

  return { secrets: secrets as PlatformEnvSecretsJson, mintedRefs, mintedKeys };
}

The selected source returns the updated structured secrets object together with the material and key names it created. Operational reporting can use those names without displaying credential values. A material that fails its own presence check aborts provisioning instead of saving a falsely complete environment.

Keep backfill distinct from rotation

Normal synchronization provisions missing materials; --rotate-secrets is the explicit rotation choice. The general overwrite flag does not imply rotation. Rotation must be coordinated with the deployed services and published verification keys that use the changed material.

The registry groups keys that belong together. A complete existing material is preserved; repairing a partially missing material follows that material’s own provisioning function. Do not remove one key from a running credential pair as a way to request routine synchronization.

Keep operator choices with the operator

The registry can generate an administrator password when one was not supplied, but it does not invent the administrator’s identity. Vendor credentials and other operator-authored values remain inputs. In a fileless CI lane, the framework reads persisted secret-store material rather than minting new credentials that disappear with the runner.

Deploy with secrets outside source control Mechanism

Production secret values can stay in the deployment’s secret store while the repository holds configuration. Wildo reconstructs the structured secret inputs from the runner’s environment and uses them to generate the runtime artifacts.

Example: Deploy with secrets outside source control

A deployment workflow receives a provider credential from its secret store. The value reaches the runtime that needs it without being written into the application’s source files.

Repository configuration and a separate secret store provide deployment inputs to the runner.
For engineers

The CI adapter reads WILDO_SECRET_* variables and validates the reconstructed object with the same schema as file-backed environments. Provider variables use WILDO_SECRET_PROVIDER_ENV_<ENV_VAR_NAME>, retaining their declared runtime name.

Generate the workflow’s variable list from the configured application workspace:

wildo config sync --env=production --domain cicd

Inspect the generated deployment workflow’s env block. It derives required and optional inputs from the secret schema and the selected providers; do not copy a fixed list from another environment.

For example, suppose a configured provider declares EXAMPLE_API_KEY as its runtime secret. The corresponding workflow fragment is:

- name: Generate deployment artifacts
  env:
    WILDO_SECRET_PROVIDER_ENV_EXAMPLE_API_KEY: ${{ secrets.WILDO_SECRET_PROVIDER_ENV_EXAMPLE_API_KEY }}
  run: npx wildo config sync --ci --env=production --domain config

This is a placeholder-only example of one entry, not the complete generated workflow. Store the real value under that secret-store name; never replace the expression with a literal credential.

BoundaryName or result
Deployment secret storeWILDO_SECRET_PROVIDER_ENV_EXAMPLE_API_KEY
Runner environmentThe same name, populated only for the generation step
Reconstructed secret inputproviderEnv.EXAMPLE_API_KEY
Configured consuming runtimeProvider credential output under its declared runtime name

The adapter validates the reconstructed inputs. Required provider inputs must be supplied; optional inputs are identified separately by the generated list. CI consumes persisted material rather than minting temporary replacements. Generated Kubernetes artifacts are written under .wildo-saas/deploy/production/k8s/ for the deployment step to consume.

Preserve key material across the boundary

The adapter normalizes escaped newlines for key and certificate variables before use. Store the actual material in the deployment’s secret manager and expose it only to the runner steps that need it. Do not replace missing persisted keys by generating fresh ones during an ephemeral deployment.

Separate source hygiene from secret operations

The framework consumes the values; the secret store owns access, rotation and expiry policy. The runner holds them in memory and may render secrets-bearing deployment artifacts, which require their own access and cleanup policy. Local file-backed environments remain a separate supported workflow; their secrets are authored inputs, not disposable generated files.

Scope each runtime

Give each runtime credentials for its own work Mechanism

Wildo provisions application credentials and narrows background-runtime material to declared access. Database, queue, cache and storage access can follow the runtime’s responsibilities instead of inheriting a shared platform credential.

Example: Limit credentials to their runtime purpose

A minion allowed to read todos receives its scoped material. It does not inherit the backend’s database credential when a narrower credential is absent.

The backend and a background runtime receive different credentials, with scoped table, queue and temporary storage access.
For engineers

The application backend uses the application’s provisioned material. Additional runtime principals derive their grants from their declarations. A minion’s resourceAccess.read and write lists govern application data; platform-access scopes are a separate axis.

Database ownership and a scoped table grant use distinct provider operations. Scoped synchronization replaces the previous authority so removing a declared resource can remove the grant instead of leaving an accumulating permission set.

Declare read and write separately

Inside the minion’s entry in wildo.saas.config.ts, an illustrative editing task would declare:

resourceAccess: {
  read: ['todos'],
  write: ['todos'],
},
Intended workDeclaration and effect
Inspect todosPut todos in read; leave write empty
Read, validate and update todosPut todos in both lists
Write without any readPut it only in write; reads still refuse, including reads needed by validation or read-modify-write logic

These identifiers name resource types, not database collection names. User-relative self-resource doors are rejected by this authoring contract. Write permission deliberately does not imply read permission.

Synchronize the changed configuration and inspect the provisioned principal’s grants. Removing a resource must remove the corresponding authority, while a read-only declaration must still refuse a write. The neighboring runtime-topology guide shows the enclosing minion registration. Storage sessions remain scoped to the application’s prefix; a database read-only grant does not also make object storage read-only.

Confine delivery as well as provisioning

The authenticated configuration fetch resolves the presenting principal. Before overlaying a non-backend principal’s material, the manager removes every path that a provider can mint per principal:

Selected source from apps-to-manager.apps-manager.platform.controller.ts:

const isApplicationBackend = principal.kind === ApplicationRuntimePrincipalKind.APPLICATION_BACKEND;

const projected: Record<string, unknown> = structuredClone(rest);
if (!isApplicationBackend) {
  for (const path of collectPerPrincipalSecretBlobPaths()) {
    AppsToPlatformAppsManagerPlatformController.deleteSecretBlobPath(projected, path);
  }
}

if (!section) {
  return projected;
}

// Overlay the principal's own material, by the same JSON paths the provider modules mint into.
for (const [path, value] of Object.entries(section.secretBlobValues)) {
  AppsToPlatformAppsManagerPlatformController.assignSecretBlobPath(projected, path, value);
}
return projected;

This selected section is from the controller’s credential projection. Stripping occurs even when the principal has no credential section. Otherwise an absent narrow grant could accidentally leave the broad application value in place. The application backend is explicitly distinguished by principal kind because that base material is its own.

Match storage access to the application identity

Object storage uses short-lived sessions with a policy tied to the platform-issued application identifier and storage prefix. Bucket provisioning remains a platform operation; a prefix-confined session does not need permission to create the bucket.

Review the resulting principal grants and delivered material when changing a runtime declaration. Credential generation, network reachability and resource-service permissions reinforce one another, but they are distinct controls with distinct consumers.

Connect each runtime to the services it needs Guarantee

Wildo derives backing-service configuration and network membership from a runtime’s declared purpose. A background runtime can receive the services it needs without automatically receiving the backend’s full data access.

Example: Connect each runtime to what it needs

A read-only background task works with todos. Its declaration gives it the corresponding data access, while a task with no application-data declaration does not acquire database access by default.

Backend and background runtimes have different declared paths to database, queue and cache services.
For engineers

Wonder Todos selects read-only application data for its marketing-scrapper minion:

Selected source from wildo.saas.config.ts:

minions: {
  marketingScrapper: {
    path: './minions/marketing-scrapper',
    resourceAccess: {
      read: ['todos'],
      write: [],
    },
  },
},

This is a selected minions entry inside the existing wildo.saas.config.ts declaration, not a standalone configuration file. A minion is a separately declared background service with tick-based execution. The example omits its independent platform-access settings. A worker is a different runtime category; do not use the names interchangeably or assume a worker is the backend’s queue consumer.

After editing the declaration, run wildo config sync from the configured application workspace. Inspect the environment generated for ./minions/marketing-scrapper and its deployment network membership. Queue, cache and telemetry are part of the minion service set; database and object-storage topology follow the presence of resourceAccess. Inspect names and grants without printing credential values.

The presence of resourceAccess activates the data-service topology. Omitting it excludes those services; an explicit { read: [], write: [] } still activates the topology but grants no resources through these lists. Reachability and resource grants are separate decisions.

Derive addresses and reachability together

resolveRuntimeBackingServiceAccess evaluates the runtime principal and declaration. The environment builder intersects that access with provisioned services; resolveRuntimeNetworkMembership uses the same result for network membership.

RuntimeBacking-service interpretation
Application backendThe application’s provisioned service set
MinionQueue, cache and telemetry, plus data services when declared
WorkerNo application backing-service capability by default
Missing or unknown principalNo granted capability

Serving HTTP is a deployment choice. Internet egress is a separate explicit declaration. A connection address does not select the application’s database engine, and network reachability does not replace operation-level permissions.

For a new runtime, inspect the generated environment and network projection together. They should express the same declared access instead of compensating for one another.

Deliver and verify

Give each process one consistent environment Guarantee

Wildo turns authored configuration and scoped secrets into the environment each process reads. A backend, frontend or background runtime receives its own generated projection, so configuration does not depend on hand-maintained copies of the same value.

Example: Give each process one consistent environment

A multiline verification key is authored once and encoded for the environment reader used by its service. The generated runtime receives the complete key rather than a truncated first line.

Configuration produces distinct environment files consumed by backend and background processes.
For engineers

Author application and infrastructure choices in their configuration files. Use configuration synchronization to regenerate derived environments; edit the source value instead of patching a generated .env.

Builders return raw values. The local dotenv writer encodes them exactly once for its reader:

Selected source from env-configuration-generator.service.ts:

function writeEnvFile(filePath: string, env: Record<string, string>): void {
  const dir = dirname(filePath);
  if (!existsSync(dir)) {
    mkdirSync(dir, { recursive: true });
  }

  const content = Object.entries(env)
    .map(([key, value]) => `${key}=${encodeDotenvValue(key, value)}`)
    .join('\n');

  writeFileSync(filePath, GENERATED_FILE_HEADER + '\n\n' + content + '\n');
}

For example, take this harmless raw value (two lines, not a real key):

PUBLIC-LINE-ONE
PUBLIC-LINE-TWO

The Node dotenv sink writes one quoted entry with an escaped newline:

EXAMPLE_PUBLIC_MATERIAL="PUBLIC-LINE-ONE\nPUBLIC-LINE-TWO"

The dotenv reader reconstructs PUBLIC-LINE-ONE followed by a newline and PUBLIC-LINE-TWO. The provider passes the original two-line value; it does not add the quotes or the escape sequence itself.

Compose and Kubernetes have their own sink encoders. A value safe for one format is not necessarily correctly escaped for another. This is why providers and runtime builders should supply the raw key or JSON value, not a pre-quoted string.

Keep runtime projections separate

The generator creates environments per process kind. Kubernetes runtime environment Secrets and provider credential Secrets have derived names shared with the workload manifests. A runtime without provider contributions does not need a provider-secret mount; it still needs its required runtime environment.

ConsumerGenerated inputWhen a change takes effect
Node backend or declared background processIts own .envRestart with the regenerated environment
FrontendIts generated .env and companion .env.exampleRestart its development process or produce the new frontend deployment
Kubernetes workloadRequired runtime environment Secret and applicable provider SecretApply artifacts and roll out the consuming pods

Generation changes an artifact, not the memory of an already-running process. Restart or roll out the affected runtime so it reads the new projection. Protect local environment files and deployment artifacts as secrets-bearing material; encoding a Kubernetes Secret is not an at-rest encryption policy.

Verify signed service calls Mechanism

On the signed application-configuration route, the manager verifies a platform service against its published public key. The issuer and intended recipient are checked alongside the signature.

Example: Give platform services their own identity

The scheduler retrieves managed application configuration from the platform manager with its signed token. The manager verifies the claimed service, intended recipient and signature before accepting the request.

Services retain separate private keys and exchange signed messages using public verification material.
For engineers

Platform initialization provisions each service’s keypair and publishes the matching public key in platform configuration. The private key belongs to the signing service’s environment. The framework-managed registry makes missing signing materials part of synchronization rather than a manual key-copy task.

On receipt, the manager checks that the token issuer matches the service identity in the request, resolves that service’s published key and verifies the token:

Selected source from apps-to-manager.apps-manager.platform.controller.ts:

await this.jwtService.verifyRaw(token, publicKey, {
  // Validate issuer matches the serviceId
  issuer: jwtServiceId,
  // Audience should be apps-manager for platform-to-platform requests
  audience: PlatformApplicationType.PLATFORM_APPS_MANAGER,
});

This selected verification call is from apps-to-manager.apps-manager.platform.controller.ts. Its surrounding path refuses an unrecognized service or missing public key. The published configuration is the verification authority; it does not silently fall back to a second environment key.

The platform-service credential retrieval route is different: it authenticates a shared platform secret and looks up the supplied registered service ID. Do not extend the signed route’s caller-to-key binding to that shared-secret route.

Keep application authentication separate

An application runtime presents its platform secret through the application door. The manager resolves the presenting runtime principal from that credential rather than trusting a principal name supplied by the caller.

A service token proves service identity and audience. The endpoint still decides what that service may do; it is not a user session or a general authorization grant.

Coordinate a key change

When rotating, distribute the private half to the signing service and publish the corresponding public half for verifiers. A mismatch is an authentication/configuration failure with a republishing remedy, not a reason to weaken verification. In local environments, wildo local init republishes platform configuration from the secret store.

Understand what is happening

Observability connects application activity to information people can use: logs that explain an event, traces that follow a request, and signals that show failures or meaningful product usage.

Wildo supplies shared collection and error-handling mechanisms. You choose the destinations, the information your application records and the operating policies around it.

An application produces records, traces and product activity with distinct purposes.

Different signals, a clearer picture

Follow activity across services

Structured logs and telemetry connect a request to the work around it. Shared context helps narrow an investigation without giving each service its own diagnostic format.

Make failures understandable

Named errors, classified logging and client-safe responses give operators useful detail and people appropriate feedback. Frontend boundaries provide a place to recover the interface.

Recognize useful activity

Browser analytics and value moments help explain product use, while model-call traces show timing and consumption. Each answers a different question and keeps its own collection policy.

Example: Investigate a slow interaction

A person reports a slow action using its support identifier. An operator follows the associated backend logs and trace to the delayed work. Browser monitoring can explain a separate interface failure; product analytics answers whether people reached the intended outcome.

For engineers

Select the question before the destination

QuestionMechanismWhat to configure
What happened in a request?Structured logs and correlation contextUseful application events and log level
Where was time spent?OpenTelemetry instrumentationEarly preload, endpoint and scoped authentication
Why did the client receive this refusal?Error builder, classifier and sanitizerNamed error type, message reference and safe details
What failed in the browser?Error boundary and selected monitoring SDKFallback placement, provider contribution and public connection
Did a person reach useful progress?Application value momentsMeaningful signals, scope and application reaction
What did model work consume?Model-call trace sinksTraceability, selected provider and content/sampling policy

Logs, product analytics and security audit evidence serve different purposes. A telemetry record is not proof that every business operation was durably recorded, and selecting a provider does not establish delivery.

Follow environment configuration into collection

This selected local infrastructure fragment shows two independent choices:

logging: {
  level: 'debug',
  formattedConsoleErrors: false,
},
observability: {
  enabled: true,
  endpoint: 'http://localhost:4318',
},

The logger’s presentation remains separate from telemetry activation. Configuration synchronization derives the process environment. The launcher preloads the OpenTelemetry SDK before application modules; the selected receiver must then accept the exported signals. Local Compose generation can supply the localhost collector, while remote environments use their authored endpoint and scoped provider secret.

The backend adds its request correlation ID to logs when a request frame exists. OpenTelemetry has its own trace ID; the two identities are connected rather than substituted. Use the service identity and support correlation to locate the intended execution, then inspect the receiver’s actual records.

Keep error handling and reporting distinct

An operation builds a named error. Its handling boundary classifies and logs it, then selects the response fields that the client may receive. Frontend error handling resolves the user message and the declared UI action. A browser provider can report browser failures externally, but it does not replace the fallback or repair the failed action.

For website SDKs, author public configuration on provider contributions and install the selected optional peer. Activation failures produce diagnostics. Verify a controlled event reaches the intended project; the declaration is only setup evidence. Collection and retention choices remain explicit application/operator responsibilities.

Measure product progress on its own terms

Value moments express meaningful application activity, not infrastructure health. Their persisted achieved state can drive onboarding without an external analytics destination. Model-call traces likewise describe usage and outcomes under a separate content policy. Keep sensitive content out unless intentionally permitted, and do not infer a complete accounting record from best-effort or sampled telemetry.

Follow backend activity

Follow activity with structured logs Mechanism

Application logs keep messages, service identity and diagnostic context as separate fields. Wildo adds the current request’s correlation ID when one is available, so related activity can be found together.

The same record can serve a developer reading a terminal and an operator filtering an observability tool. You choose which application events deserve a log and what information belongs in their context.

Example: Find the work behind a failed request

A support report includes a correlation ID. Filtering backend logs by that ID brings together the request’s application messages and its handled error without searching for fragments of a person’s name.

Application events share a structured record format.
For engineers
Write fields that can be queried

Use the injected LoggerService in a backend service. Supply an object as context; do not stringify it before logging. This illustrative service-body excerpt assumes logger is the injected logger and the surrounding application owns the operation:

logger.info('Document generation started', {
  documentId,
  templateRef,
  format: 'pdf',
});

// The operation's result is already available here.
logger.info('Document generation completed', {
  documentId,
  durationMs,
  outputBytes,
});

The message describes an event; the fields make two runs comparable. These are application-selected diagnostic fields, not an instruction to log complete records, credentials or document contents. Logger redaction covers configured paths; it cannot discover every sensitive value inside arbitrary application data.

Understand what the logger adds

LoggerService.createLogger() reads request context at emission time. This source excerpt is the correlation portion of its Pino configuration:

mixin: () => {
  const frame = getRequestContextFrame();
  if (!frame) {
    return {};
  }
  return {
    correlationId: frame.correlationId,
    ...(frame.frontendServiceName ? { frontendServiceName: frame.frontendServiceName } : {}),
    ...(frame.clientInstanceId ? { clientInstanceId: frame.clientInstanceId } : {}),
  };
}

Service and environment identity are configured separately on every record. A startup log outside a request has no request frame; do not invent a correlation ID simply to fill a column. Wildo’s correlation ID is distinct from an OpenTelemetry trace ID, although instrumentation can connect the two.

Preserve useful error structure

For a handled, best-effort catch, use projectCaughtErrorLogFields(error) from @wildo-ai/saas-backend-lib in the context. It preserves classified Wildo errors, native error details and structured validation issues. The logger normalizes nested values rather than burying another JSON string inside JSON.

For a failure you propagate to an existing request boundary, let that boundary log it. Repeatedly logging and rethrowing creates several apparent incidents from one failure. See error severity and telemetry export for those separate steps.

Keep local output readable and inspectable Mechanism

Local development labels and captures each service’s output while presenting it in the terminal. Choose structured output when diagnostic tools need to read individual log fields.

Keep console formatting disabled for machine-oriented checks. Use the process label to locate the same event in the terminal and capture.

Example: Inspect a validation failure

With structured process output enabled, a validation error keeps its fields in the captured record. A diagnostic tool removes the process label, parses the JSON and examines the issues without extracting them from prose.

The same process output produces raw machine records and readable terminal output.
For engineers
Choose presentation in the environment

Wonder Todos authors this logging block in its local infrastructure configuration:

logging: {
  level: 'debug',
  formattedConsoleErrors: false,
},

false keeps the process logger’s JSON output and the terminal’s raw presentation. true also enables pretty formatting inside the process logger, before the CLI receives stdout. The capture then contains those received text lines, not the original JSON record.

SettingProcess outputCaptured form
formattedConsoleErrors: falseStructured logger recordsProcess-prefixed, ANSI-free JSON lines
formattedConsoleErrors: truePretty logger outputProcess-prefixed text, potentially several lines per event

Use the first setting for tools that require structured fields. The CLI’s recognition of a JSON record cannot recover fields already turned into prose upstream.

Follow one line to both destinations

LocalDevOutputBoundary.emitLine() performs the split. This selected implementation excerpt keeps the decision visible:

const pressure = new Set<LocalDevOutputSink>();
if (this.options.writeCapture(`[${this.options.label}] ${stripAnsi(line)}\n`) === false) {
  pressure.add(LocalDevOutputSink.CAPTURE);
}

const consoleLines = this.options.consoleRendering === LocalDevConsoleRendering.RAW
  ? [line]
  : formatLocalDevConsoleLine(line);

for (const consoleLine of consoleLines) {
  if (this.options.writeConsole(`${this.options.consolePrefix}${consoleLine}\n`) === false) {
    pressure.add(LocalDevOutputSink.CONSOLE);
  }
}

The capture is process-prefixed and ANSI-free, not an unchanged byte-for-byte copy of the pipe. A tool reading JSON must account for that label. The human renderer recognizes structured log records; unrelated text passes through rather than being guessed into a record.

Keep framing and flow together

Separate UTF-8 framers own stdout and stderr. A chunk split through a character or a line is held until it can be emitted correctly; one stream cannot finish the other’s partial record. LocalDevOutputFlowController pauses both streams when either destination applies back-pressure and resumes after the pressured sinks drain.

This is the local stack’s output handling. Production collection, retention and log access are operator concerns. When investigating a formatting discrepancy, compare the capture with the terminal rather than assuming the decoded view is the stored record.

Connect traces, metrics and logs Mechanism

Backend activity can produce connected traces, measurements and log records through OpenTelemetry. Wildo starts the instrumentation before application code and sends those signals to the configured OTLP endpoint.

This helps an operator move from a slow request to the calls and messages around it, while application code keeps a consistent logging interface.

Example: Explain where a request spent its time

A request feels slow. Its trace identifies time spent in instrumented dependencies, while related logs provide the application’s own context. The operator can investigate the relevant step rather than treating the whole request as one unexplained delay.

Request activity produces traces, metrics and logs for a telemetry endpoint.
For engineers
Configure the runtime before it starts

In the environment configuration, author the endpoint the process can reach. This selected Wonder Todos local configuration sends to its local collector:

observability: {
  enabled: true,
  endpoint: 'http://localhost:4318',
},

Configuration synchronization projects this onto OTEL_ENABLED, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME and resource attributes. Provider authentication is a separate scoped input. A container may need the generated internal service address rather than the host’s localhost address.

The runtime launcher must preload @wildo-ai/saas-backend-lib/otel-preload with Node’s --import before instrumented modules load. Setting an endpoint alone does not start the SDK. The preload reads its early environment and only activates when OTEL_ENABLED enables it.

See the three signal paths

This selected SDK configuration comes from otel-preload.ts; instrumentation options are omitted here:

const sdk = new NodeSDK({
  serviceName: process.env.OTEL_SERVICE_NAME ?? 'wildo-saas-backend',
  traceExporter: new OTLPTraceExporter(),
  metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),
  logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())],
  // Auto-instrumentation and the Pino bridge are configured here too.
});

Automatic instrumentation supplies dependency spans and measurements. Pino instrumentation adds a log export destination while retaining the original output stream. Filesystem and low-level network instrumentation are disabled in the preload to reduce noise. Framework counters cover additional specific operations; application-specific measurements require their own deliberate instrumentation.

Check activation, then delivery

ObservabilityBackendService compares selected telemetry intent with the SDK handle published by the preload. A mismatch produces a diagnostic rather than pretending collection is active. The service registers SDK shutdown with the existing shutdown coordinator, including a telemetry-enabled host without a queue; short-lived natural exits also have a preload backstop.

A loaded SDK is only the first check. Confirm that the intended collector receives the signals from the correct service. An unreachable endpoint, authentication failure or abrupt process kill can lose telemetry. Export is observation of application work, not durable business accounting or automatic incident repair.

Choose where telemetry goes Mechanism

Telemetry collection uses a common protocol while its destination and authentication are configured separately. Wildo translates the selected provider’s connection contract into the environment read by OpenTelemetry.

You can keep instrumentation consistent while choosing the receiving service and the runtime scopes allowed to use its credentials.

Example: Use a local receiver and a hosted destination

Development sends signals to a local collector. A remote environment points at its configured telemetry endpoint and supplies the receiving provider’s scoped credential. The application’s log calls do not change.

Telemetry can be directed to the selected receiver.
For engineers
Separate the endpoint from the credential

Author observability.enabled and observability.endpoint in the environment. Select the telemetry capability/provider for the application and explicitly include that provider in each runtime scope that needs authenticated export. The runtime’s resolved provider-secret bag is the authority for the token; selection alone does not widen its access.

BetterStack’s provider declares the following connection contract. This is an excerpt of the provider implementation, not application configuration to duplicate:

secretsContract: {
  sourceToken: {
    envVarName: 'BETTERSTACK_SOURCE_TOKEN',
    required: true,
    description: 'BetterStack Telemetry source token (Bearer) for OTLP traces/metrics/logs ingestion.',
  },
},
otlpAuth: {
  headerName: 'Authorization',
  valueTemplate: 'Bearer {token}',
  secretContractKey: 'sourceToken',
},
protocols: [],

The endpoint is environment-specific connection information. The source token is secret material. The provider maps that token into an OTLP authorization header; the preload has no BetterStack-specific client or switch.

Understand the generated process input

For an authenticated runtime, the resulting shape is equivalent to the following schematic environment. The endpoint and token here are placeholders, not working credentials:

OTEL_ENABLED=true
OTEL_SERVICE_NAME=application-backend
OTEL_EXPORTER_OTLP_ENDPOINT=https://telemetry.example.invalid
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer REPLACE_WITH_SCOPED_TOKEN

Do not hand-maintain a competing environment file: the configuration builder emits the runtime-specific values. If the runtime did not receive the provider secret, it must not recover a broader token through another lookup. It may still attempt unauthenticated export to its endpoint, which is useful for a local collector but rejected by an authenticated vendor.

Verify the connection actually receives signals

Provider registration states how connection information is resolved. It does not establish that an endpoint exists, accepts these credentials, retains records or supplies dashboards. Check the emitted service identity, scoped authentication and receiver when diagnosing a quiet dashboard. LLM-call observability is a separate provider capability and content policy, even when the same vendor offers both products.

Inspect telemetry locally Mechanism

A local OpenTelemetry collector gives development processes a nearby receiver for traces, metrics and logs. Wildo’s Compose generation includes it when telemetry is enabled and the configured endpoint points to localhost.

This lets a team inspect the telemetry path before connecting a remote receiver.

Example: Check that a backend emits signals

A developer enables a localhost endpoint, starts the generated stack and performs an application action. Collector output shows whether records arrived from that backend; a configured endpoint alone would not establish that.

A local collector receives application telemetry.
For engineers
Make the collector an environment decision

The selected local environment block is:

observability: {
  enabled: true,
  endpoint: 'http://localhost:4318',
},

The Compose generator parses the endpoint and checks its hostname. localhost and 127.0.0.1 activate local collector inclusion; a remote destination does not silently add a redundant local collector. An explicit endpoint port determines the published host port, with the OTLP/HTTP default when omitted.

Follow the generated service

This selected Compose template excerpt shows the collector’s image, configuration and published-port relationship:

otel-collector:
  image: otel/opentelemetry-collector-contrib:0.154.0
  container_name: wildo-otel-collector
  restart: unless-stopped
  mem_limit: 512m
  configs:
    - source: wildo-otel-collector-config
      target: /etc/otelcol-contrib/config.yaml
  ports:
    - "{{otelCollectorHostPort}}:{{otelCollectorInternalPort}}"

The braces are generator substitutions, not values to paste into a completed Compose file. The template also joins the collector to the telemetry network with a dedicated internal alias. Host processes and containers therefore use addresses appropriate to their network context.

Inspect receiving independently of application configuration

After the existing local stack is running, a bounded read of its collector output can show received signals:

docker logs --tail 2000 wildo-otel-collector 2>&1

Include stderr: a debug exporter can write there. First confirm a known signal is visible, then look for the intended service and trace; an empty filtered stdout stream is not proof of missing telemetry. Increase the bounded window only when needed to reach the event you are investigating.

The receiver is not a searchable long-term store, a dashboard or an alerting policy. Those come from the destination you operate. Keep the selected environment, generated networking and application preload aligned rather than starting a competing collector on the same port.

Understand failures

Give failures a consistent meaning Mechanism

A backend failure carries a named type, message reference and context instead of relying on a free-form exception string. Wildo uses that shared contract to shape the response and guide its handling.

Application code states what went wrong; clients and operators receive information suited to their different needs.

Example: Refuse an action that breaks a business rule

A service rejects an action using the business-rule error type. The client receives the corresponding message reference, while diagnostic context helps the operator understand which action was refused.

A named backend error carries a stable identity and structured details.
For engineers
Build a deliberate rejection

Inside a custom operation, use the supplied utils.errorBuilder and the current executionContext. This illustrative operation-body excerpt uses the shared error vocabulary; canProceed and recordId are values supplied by that operation:

if (!canProceed) {
  throw utils.errorBuilder.buildError(ErrorType.BUSINESS_RULE, executionContext, {
    customMessageReference: ErrorCustomMessageReference.BUSINESS_RULE,
    context: {
      recordId,
      operation: 'approve',
    },
  });
}

Import ErrorType and ErrorCustomMessageReference from @wildo-ai/saas-models. The reference chooses translated user wording; structured context explains the condition for diagnostics. A custom message needs a defined reference and translations, not a sentence smuggled into context.

Follow the contract to its consumers
Authored informationConsumer and effect
Error typeShared definition supplies status, severity and handling policy
Message referenceFrontend translation resolves the user-facing explanation
Execution contextBuilder records operation and identity context
Correlation IDRequest-local fallback connects an error to its surrounding activity
Allowed structured detailsClient serialization retains only the relevant context subset

A missing explicit execution context does not mean inventing a new request identity: the builder uses the ambient request frame when present. A Wildo backend error is a structured object, not necessarily a native Error instance.

Preserve an already-classified failure

Use isWildoBackendError(error) when branching on a caught value. The builder’s conversion path preserves a classified error’s type and context rather than replacing it with an “unknown” error. Native exceptions are converted at the boundary, but their classification may depend on name/message heuristics; that is why authored request rejections should be explicit.

Error declarations describe several handling policies. A declared action is not evidence that an alerting or recovery integration is operating. Logging is deliberately owned by the handling boundary, not error construction. For a form field, prefer schema validation so the field-level error map can identify the affected input; use a business-rule rejection for a condition spanning the action.

Make important failures stand out Mechanism

Wildo classifies handled errors so routine client conditions do not look like server outages. The boundary that handles a failure owns its log entry, with structured fields and a stack when the level warrants one.

That keeps expected refusals visible without drowning operational faults in repeated exception messages.

Example: Distinguish an expired session from an outage

An expired access token that the client can refresh is logged at debug. A failed authorization is a warning. A server fault is an error with a stack, making the situations easier to separate during investigation.

Classified errors retain their severity at the handling boundary.
For engineers
Read severity as an operating signal

The classifier combines the error’s declared status and severity, with a narrow exception for transparent client recovery:

ConditionEmitted levelStack in classified boundary log
Recognized self-healing expired JWTDebugNo
Client condition with low severityDebugNo
Client condition with medium severityWarnNo
Server status or higher severityErrorYes

A bad token signature and an edit conflict are not transparent recovery cases. A queue boundary can deliberately use its own operational level while reusing the canonical error-field projection: a dead-lettered job remains important regardless of the underlying request-shaped classification.

Emit at the handling boundary

This source excerpt from the classified logging helper shows the shared fields and conditional stack. The caller supplies the boundary-specific context:

const level = classifyBackendErrorLogSeverity(error);
const mergedContext: Record<string, unknown> = {
  ...projectWildoBackendErrorLogFields(error),
  ...context,
};
const finalContext: Record<string, unknown> =
  backendErrorLogSeverityIncludesStack(level) && typeof error.stack === 'string'
    ? { ...mergedContext, stack: error.stack }
    : mergedContext;

The helper then dispatches once to the logger method corresponding to level. Error construction emits no log; application services should propagate request errors to their existing boundary instead of logging the same failure at every layer.

Keep swallowed faults visible too

For best-effort catches that handle rather than propagate a classified Wildo backend error, projectCaughtErrorLogFields(error) carries a severity floor. LoggerService can raise a requested debug/info/warn record to that floor and records promotedFromLevel. A classified Wildo server fault therefore does not disappear into warn merely because the caller expected the ordinary case to be non-fatal. Native errors and other unknown values retain structured diagnostic detail but do not acquire this classifier-derived floor; the caller must select their appropriate level.

The floor applies through that projection/logger path; it is not global interception of every console write. Classification also does not create monitoring rules or increment a production alert counter. Configure the receiving system’s queries and policies around the actual records it receives.

Keep internal error details on the server Guarantee

Client error responses keep the information needed to understand a refusal or correct an input, while internal context is filtered before serialization. Wildo applies a shared rule instead of asking every endpoint to decide what to expose.

Operators retain diagnostic information; clients receive a structured response with a message reference and support correlation ID.

Example: Explain a failed save safely

A form can receive field-level validation feedback. A database failure follows a different path: its internal context is removed from the client response, while the support identifier helps locate the server-side event.

Detailed server errors are reduced to client-safe response information.
For engineers
Know which context may cross the boundary

sanitizeErrorForClient() treats validation, conflict and rate-limit context as candidates for an explicit key allow-list. Authentication uses a narrower list, including specific lockout feedback. Other families remove their context. This selected source excerpt shows the default branch:

// All other types (AUTHZ, INTERNAL, DATABASE, etc.): strip context entirely
const { context, ...rest } = wildoError;
return rest as WildoError_Backend;

The sanitizer removes context; the response serializer separately chooses public fields and omits the backend stack. Do not interpret the returned backend-shaped object as a response DTO to send wholesale.

Follow serialization, not just the filter

This excerpt selects fields from the actual ErrorHandlerService response assembly; other response metadata is omitted:

const response: ErrorResponse = {
  error: {
    type: saasError.type,
    message: saasError.customMessageReference,
    ...(typeof sanitizedError.context?.code === 'string' && { code: sanitizedError.context.code }),
    correlationId: saasError.correlationId ?? getCurrentCorrelationId(),
    ...(sanitizedError.context && { details: sanitizedError.context }),
    ...(validationErrors && { validationErrors }),
    // Other selected response metadata remains in the implementation.
  }
};

This is a selected implementation excerpt, not a complete typed replacement. Validation issues are extracted before context filtering into a separate field map. An allowed stable code is promoted to error.code, allowing a client to branch on a declared identifier instead of parsing translated text.

Author safe details at their source

The allow-list checks key names, not the sensitivity of arbitrary values placed under them. Keep driver messages, stacks and secret material out of public fields; use a named shared code when clients need a specific branch.

Response filtering and authorization are different contracts. A filter cannot make two responses indistinguishable if upstream code selects different statuses or exposes different existence information. The response also retains selected metadata such as operation and initiator information; it is not an anonymous two-field envelope. Review the concrete route’s response when making a confidentiality claim.

Give failed screens a useful fallback Mechanism

React error boundaries replace a failed part of the interface with an error state. Wildo connects that fallback to its shared error context and declared frontend actions, so failures can be handled consistently.

Applications can customize the fallback presentation while keeping the common handling path.

Example: Keep navigation around a failed panel

A rendering failure inside a boundary replaces its child panel with a fallback. The rest of the surrounding interface can remain available, and the error follows the same frontend event path as other handled failures.

An interface failure has a local fallback and error context.
For engineers
Place the boundary around the failure region

ErrorBoundary catches errors in its descendant React rendering tree. Its default fallback uses the application layout slot; fallback and onError are customization points. The default fallback uses routing context, so mount it within the application’s normal router/provider setup.

This selected source excerpt shows the step after React reports an error:

const wildoError = sharedErrorBuilder.buildReactError(error, errorInfo);

try {
  frontendEventBus.emit(FrontendEvents.ERROR_OCCURED, {
    error: wildoError
  });
} catch (dispatchError) {
  logger.warn('Failed to emit error event:', {dispatchError});
}

if (this.props.onError) {
  try {
    this.props.onError(error, errorInfo);
  } catch (handlerError) {
    logger.warn('Error in custom error handler:', handlerError);
  }
}

The native error and React component information are converted into the shared frontend shape. A failure in reporting is guarded so it does not replace the original rendering failure with another unhandled error.

Separate catching from the user response
PartResponsibility
Component boundaryCatch descendant render failures and render a fallback
Router boundaryPresent route-level failure within the routing surface
Error providerKeep the builder’s user/organization context current
Global handlerResolve user wording and execute declared frontend actions
Application slot/presetDecide the appearance of the fallback

ErrorProvider and the global handler connect errors to notification, navigation and session actions. The message comes through the shared error/i18n path; a custom fallback should preserve meaningful recovery choices rather than exposing a raw stack.

Handle other failures through their own path

A React boundary does not catch arbitrary rejected promises or errors in event handlers. Those must be caught and passed through the application’s error handling path. Resetting a fallback retries rendering; it does not repair corrupt data or guarantee that the next render succeeds.

The frontend logger and event handling provide local diagnostics. Selecting a remote browser-monitoring provider is a separate integration; this boundary alone does not send an incident to Sentry.

Connect website errors to a monitoring service Mechanism

A selected browser-monitoring provider loads and starts its SDK on the website. Wildo supplies the activation lifecycle and configuration boundary, while the provider supplies the actual error-monitoring integration.

This connects browser failures to a receiving service without embedding vendor initialization throughout the website.

Example: Investigate a browser-only failure

A configured Sentry integration can receive a browser exception that never became a backend request. The team investigates it in the receiving project while the website’s own fallback remains responsible for the visitor’s experience.

Browser failure reports reach a selected monitoring destination.
For engineers
Configure the selected website contribution

The shipped website provider is SentryFrontendWebsiteProvider. Select it for the website’s frontend error-monitoring capability, install its optional @sentry/browser peer where the website is bundled, and supply the project’s public DSN on its provider contribution.

This illustrative fragment belongs inside the application’s existing contribution mapping. It assumes contribution is the selected Sentry entry and dsn is the deployment’s public DSN:

return {
  ...contribution,
  frontendEntry: {
    ...contribution.frontendEntry,
    publicConfig: {
      ...contribution.frontendEntry.publicConfig,
      dsn,
    },
  },
};

Use the contribution channel, not invented publicConfig fields in the strict provider-selection block. The module declares the DSN as the source of its CSP connection origin, so connection policy follows the configured destination. Never put a private administration token in browser public configuration.

Follow activation to the vendor

The provider validates a non-empty DSN before its dynamic import. Its actual initialization and cleanup are:

const Sentry = await import('@sentry/browser');
const client = Sentry.init({ dsn });
return {
  handle: client,
  deactivate: async () => {
    await client?.close();
  },
};

The shared runner activates only in a browser. An activation failure emits a diagnostic and does not prevent other providers from starting. useWebsiteProviderSdks also handles an unmount while an asynchronous activation is still finishing, then closes the returned client.

Check the correct proof

An installed package, selected provider and non-empty DSN are setup evidence. Confirm receipt in the intended project using a controlled browser failure before claiming monitoring is operating. Wonder Todos’ source demonstrates the contribution mechanism but deliberately leaves Sentry’s DSN unconfigured; it is not evidence of a live Sentry project.

This integration is the website SDK path. React application error boundaries, backend telemetry and security audit export are separate mechanisms. Activation lifecycle support also does not by itself define the application’s consent policy.

Understand product activity

Connect website activity to product analytics Mechanism

A selected analytics provider can load its browser SDK through the website’s provider system. Wildo keeps activation, public configuration and cleanup in one integration path instead of scattering vendor scripts across pages.

You choose the receiving project and the analytics policy; the provider connects the browser to it.

Example: Observe website activity in the selected project

A website uses its configured PostHog project and endpoint. The SDK receives activity according to its configured behavior, while the application remains responsible for choosing which collection is appropriate.

Selected browser events reach a product analytics destination.
For engineers
Supply the browser integration’s configuration

The PostHog website provider declares posthog-js as an optional peer. Install it in the website’s bundling environment, select the product-analytics provider, and configure its public project key and endpoint through the application’s contribution mapping.

This illustrative mapping fragment assumes the current contribution is PostHog and the deployment supplied publicProjectKey and apiHost:

return {
  ...contribution,
  frontendEntry: {
    ...contribution.frontendEntry,
    publicConfig: {
      ...contribution.frontendEntry.publicConfig,
      apiKey: publicProjectKey,
      apiHost,
    },
  },
};

These are browser-visible settings. They are not private account-management credentials. The provider derives a CSP connection origin from apiHost; configure the actual receiving host rather than adding a guessed vendor-origin list.

Understand what initialization does

The shipped module validates the key, imports the SDK on demand and returns its cleanup behavior:

const { default: posthog } = await import('posthog-js');
posthog.init(apiKey, typeof apiHost === 'string' ? { api_host: apiHost } : {});
return {
  handle: posthog,
  deactivate: () => {
    posthog.opt_out_capturing();
  },
};

The shared runner isolates activation failures. The website hook deactivates when the registry/lifecycle changes, including when a page unmounts before initialization completes. This is cleanup support; the hook does not itself decide when a visitor has consented.

Keep separate analytics systems distinct
SurfaceWhat it does
Website PostHog SDKLoads the selected browser analytics integration
Application analytics contextTracks configured application events and evaluates value moments
LLM observability sinkReports model-call usage/timing under its own content policy

Wonder Todos supplies its PostHog host through the contribution channel, but its source deliberately omits the project key. Use that file as a configuration pattern, not proof that live events are reaching a project. Verify the selected destination and collection behavior in the browser before describing the integration as operating.

Recognize when people reach meaningful progress Mechanism

Value moments describe activity that matters to your product, such as first value or an activation step. Wildo matches declared signals and exposes achieved moments for the application to use.

You define what counts as progress; the framework connects those definitions to application events and persisted achievement state.

Example: Recognize a first useful action

A product defines its first completed action as a value moment. Once the matching signal is observed, an onboarding surface can reflect the achievement instead of inferring progress from page visits alone.

Application-defined signals establish meaningful product progress.
For engineers
Declare the meaning before collecting the signal

Use the application’s shared analytics configuration and enable its analytics capability. This illustrative declaration uses a custom event, so the application controls exactly when the meaningful action has happened:

analytics: {
  enabled: true,
  providers: [],
  valueMoments: [{
    identifier: 'first-useful-result',
    role: ValueMomentRole.TTFV,
    scope: ValueMomentScope.USER,
    mode: ValueMomentMode.ANY,
    signals: [{
      signalType: ValueSignalType.CUSTOM_EVENT,
      eventName: 'useful-result-completed',
    }],
    onAchieved: {
      frontendEvent: true,
    },
  }],
},

Import the four enums from @wildo-ai/saas-models. This is an illustrative configuration fragment, not an existing Wonder Todos moment. An empty provider list allows internal progression without selecting an external analytics destination; frontend evaluation still follows the analytics context’s activation and consent conditions.

After the successful application action, useAnalytics().track('useful-result-completed', properties) supplies the custom event. Emit success only after the action succeeds, and keep its properties appropriate for the selected collection policy. A backend can emit its own custom signal through ValueMomentEngineBackendService.emitSignal when that is the relevant source.

Choose signals and scope deliberately
ChoiceMeaning
Resource operationMatch the named operation, optionally with shallow field filters
Resource stateCompare a scoped record count with a threshold
View/feature/custom eventObserve a browser or explicitly emitted application event
ANY / ALLOne qualifying signal or all declared signals
User / organizationWhose progression state owns the achievement
Anti-patternA repeatable signal with cooldown, not a persisted achievement

A record-state signal needs the expected scope field and repository. The backend applies the moment’s scope when counting. Filters compare primitive values by shallow equality; they are not a query language.

Match the example to its evaluator

The single custom-event ANY example above does not need occurrence counting or multi-signal accumulation. More elaborate declarations cross different evaluation paths:

ContractBrowserBackend
Custom/operation occurrence thresholdCounts occurrencesCurrently handles the first matching event without that counter
Anti-pattern prerequisites and stage suppressionEvaluated before reactionThe anti-pattern branch goes directly to scope/cooldown handling
Anti-pattern ALL compositionAccumulates required signalsA matching signal can invoke the reaction without accumulating ALL
Ordinary ALL progressAccumulates observed signalsPersists progress, but concurrent partial writes can overwrite each other

These distinctions matter when choosing which side owns a product reaction. Do not base access or accounting on analytics progression. A single-event progress indicator and a coordinated multi-event workflow need different guarantees; the latter should have its own application state and transaction rules.

Follow persistence and the reaction

Ordinary achievements are stored on the progression-state row in valueMomentsAchieved, separately from authored milestones. Backend writes guard against an existing achievedAt. getAchievedMoments reads user and organization state; the browser’s backend-sync path restores more than local storage alone.

The achievement callback can raise the frontend event or resolve a registered backend handler. Register a named handler before referring to it. The persistence outcome distinguishes recorded, already achieved, absent scope and failed persistence; do not treat an accepted HTTP request as an unqualified accounting guarantee.

ANTI_PATTERN intentionally does not persist achievement and uses an in-process cooldown. This is product guidance and behavioral measurement, not an authorization decision or a financial ledger. Design the application reaction accordingly.

See what model calls consumed Mechanism

Model calls can leave a consistent trace of usage, timing and outcome. Wildo supplies a structured-log destination and can forward traces to a selected observability provider.

This makes AI behavior easier to compare across features without giving every caller its own telemetry format.

Example: Explain a slow response

A team inspects the call’s duration and token usage to distinguish a large model response from an application delay.

A model call leaves timing and usage information for configured observability consumers.
For engineers

Enable the observability capability and select its provider in the runtime scope that makes the model calls. These excerpts adapt Wonder Todos’ configuration; they belong in three existing sections of wildo.saas.config.ts:

// In engineCapabilities:
[EngineCapability.LLM_OBSERVABILITY]: { enabled: true },

// In the runtime scope's providers:
posthog: {
  engineCapabilities: [EngineCapability.LLM_OBSERVABILITY],
  providerCapabilities: ['LLM_OBSERVABILITY'],
  protocols: [],
},

// In the same scope's selection:
[EngineCapability.LLM_OBSERVABILITY]: {
  primary: 'posthog',
  whenUnavailable: [],
},

Supply the provider’s POSTHOG_PROJECT_API_KEY through runtime secret configuration and install its declared posthog-node dependency in that runtime. POSTHOG_HOST selects the provider endpoint when needed. Observability has no request/response exchange protocol here; its provider supplies a trace sink.

Author content capture and sampling in the environment’s infrastructure configuration. This illustrative fragment keeps content capture off and requests all calls; it is separate from the provider declaration and the OTLP activation switch:

observability: {
  llmObservability: {
    captureContent: false,
    sampleRate: 1,
  },
},

Configuration synchronization projects this policy into the executing process’s environment. The following shows its generated result, not a file to maintain by hand:

# WILDO_LLM_OBS_CAPTURE_CONTENT is omitted; capture defaults to false.
WILDO_LLM_OBS_SAMPLE_RATE=1

The absent capture flag keeps prompts and completions out of provider capture; 1 requests no deliberate sampling drop. Delivery still remains best effort. Local structured-log content is separately gated by AgentTraceabilityLevel.DEBUG_VERBOSE; turning on provider capture does not change that logger policy.

Understand the inherited trace

Trace sinks consume AgentCallTraceEmission. The built-in logger extracts usage, duration and outcome from that envelope. This selected implementation excerpt shows ordinary metadata and the separate verbose-content gate:

this.logger.info('agent-call-trace', {
  agentRef: trace.agentRef,
  outcome: trace.outcome,
  ...(trace.attempt ? { attempt: trace.attempt } : {}),
  ...(trace.context?.usage ? { usage: trace.context.usage } : {}),
  ...(trace.context?.executionTime != null ? { executionTimeMs: trace.context.executionTime } : {}),
  ...(trace.context?.finishReason ? { finishReason: trace.context.finishReason } : {}),
  ...(trace.error ? { error: trace.error } : {}),
  ...(context?.attributes ? { attributes: context.attributes } : {}),
  ...(verbose ? { input: trace.input, output: trace.output } : {}),
});

verbose is determined from the resolved AgentTraceabilityLevel.DEBUG_VERBOSE policy. Selecting richer trace content is a deliberate policy decision, not a prerequisite for token and latency metadata.

Add a provider when you need aggregation

The observability sink resolves the configured provider lazily. The PostHog implementation accepts sampling and a separate captureContent choice; content capture starts off. Its runtime client is an optional dependency required where that provider actually executes.

Generation and embedding records differ. Generation usage includes prompt and completion tokens; embeddings summarize input use and vector dimensions. Raw embedding vectors are excluded from analytics even when content capture is enabled.

Use telemetry for observation

Composite sinks isolate failures and flush children during shutdown. A failed analytics sink should not fail the model call it observes. Sampling and best-effort delivery mean provider records are operational signals, not a complete accounting ledger. Budgets are enforced by the separate admission mechanism, not by the existence of a telemetry event.

Know when a service can do its work

A running process is only part of a working application. Its dependencies must be ready, its access usable and its workload manageable.

Wildo connects readiness checks, credential handovers and orderly shutdown with explicit request and page limits. Operators can distinguish an answering process from its evaluated dependency state, and plan changes around work already underway.

You choose deployment responses, operating thresholds and recovery procedures. The framework supplies the signals and mechanisms those decisions use.

Readiness, orderly change and bounded requests support service operation.

Make operating conditions visible

Check before serving

Readiness names failures among its configured checks, while startup reports reveal missing administrative access. Each signal answers a specific operating question.

Change without an abrupt cutover

Credential overlap gives consumers time to migrate. Coordinated shutdown stops new work before draining active jobs and releasing resources.

Keep requests bounded

Request windows control frequency and page ceilings control response size. Choose both around the operation’s purpose and cost.

Example: Prepare a release around the work in progress

A replacement process receives its own service access and passes readiness before receiving traffic. The retiring queue consumer stops intake and gives accepted jobs a bounded opportunity to drain. If an integration key also changes, a separate rotation window gives its consumer time to adopt the replacement.

For engineers

Give each mechanism its own responsibility

MechanismWhat it establishesWhat you configure or operate
LivenessThe process can answer its probeProcess supervision policy
ReadinessThe runtime’s evaluated dependencies are readyTraffic eligibility and dependency investigation
Administrative continuity reportMissing or fragile usable administrative access at startupRecovery authority and monitoring of evidence
Service credential provisioningRegistered consumers receive their connection materialProvisioner authority and environment setup
Rotation windowOne prior API-key/client secret can authenticate before its deadline while the credential is active and unexpiredSecure handover to the consuming integration
Job drainAccepted work receives a bounded shutdown opportunityTermination budget and interrupted-job semantics
Request and page limitsDeclared traffic and result bounds are appliedMeaningful limits and expensive-query design

Investigate readiness before changing the process

Use /health/ready to identify the failing component; use logs for the detailed cause. An unanswered probe, a 503 with component statuses and an application-level business refusal are different observations. Restarting a process is not a substitute for understanding which dependency is unavailable.

The built-in readiness evaluation covers initialized configuration, conditional MongoDB and storage. Additional dependencies join through a bound contributor; PostgreSQL has no built-in continuing check here. Choose checks that reflect this host’s traffic requirements.

Treat change as a sequence

Prepare credentials and dependencies before admitting a replacement runtime. Stop intake before releasing a retiring worker’s connections. Keep credential rotation distinct from shutdown: the previous secret’s deadline belongs to the integration handover, while the job-drain deadline belongs to process termination.

The capability guides below show the actual contracts and extension points. They explain the available observations without implying that readiness repairs a service or that a drain guarantees every job completes.

Check before serving

Know whether a process is ready to serve Guarantee

A process can answer requests while a service it relies on is unavailable. Wildo separates liveness from readiness so a deployment can distinguish those situations.

Readiness evaluates configuration and its registered dependency checks with individual deadlines. Its response identifies unhealthy components; diagnostic logs hold the detailed cause. Your deployment decides how to use that signal to route traffic or investigate a failure.

Example: Keep an unavailable dependency visible

A backend answers its liveness probe, but file storage is unavailable. Readiness returns an unsuccessful status with the failing component named. The operator can investigate storage without treating the backend’s ability to answer as proof that it can do useful work.

A process answering and its dependencies being ready are two separate checks.
For engineers
Read the right endpoint

The engine mounts GET /health and GET /health/ready at the root of the HTTP service. Use readiness for eligibility to receive work, and liveness for the process answering. These are separate deployment inputs, not interchangeable tests.

ProbeObservationResponse behavior
/healthThe HTTP process can answerHealthy response with uptime and version
/health/readyThe configured dependency checks passHTTP 200 when ready, otherwise 503; includes components and failing
Follow the actual response contract

This excerpt is from the engine readiness handler; readiness comes from evaluateReadiness(). The response deliberately exposes component names and coarse status, while driver error text stays in logs.

const response: ReadinessResponse = {
  status: readiness.status,
  timestamp: new Date().toISOString(),
  uptime: process.uptime(),
  version: process.env.npm_package_version || '1.0.0',
  components: readiness.components,
  failing: readiness.failing,
};

res.status(readiness.ready ? 200 : 503).json(response);

A probe consumer can therefore distinguish a status failure from an unanswered request and locate the component involved. Read the associated warning for connection details; do not put secrets or raw provider error strings into a public health response.

Inspect a response before changing routing
curl -i http://localhost:4241/health/ready

Use the port of the backend being examined; 4241 is the Wonder Todos backend example. Read the HTTP status together with components and failing. A 503 with a named component is different from a connection refusal or timeout: the former is a completed readiness evaluation, while the latter needs process/network diagnosis.

Choose dependencies by actual runtime need

The evaluation includes initialized application configuration and file storage. MongoDB is conditional on the runtime’s dependency requirements. Redis is not a universal gate because some runtime paths allow an in-memory fallback. Checks run in parallel and each has a five-second deadline.

The built-in list is finite:

CheckCoverage
Application configurationInitialization state
MongoDBConditional on the runtime’s MongoDB requirement
File storageIts readiness evaluator, including its configured skip conditions
ContributorAdditional checks supplied by the process container

There is no built-in PostgreSQL readiness probe in this controller. Its startup schema validation is a different event, not continuing connectivity evidence. If PostgreSQL availability should control this host’s traffic eligibility, supply and bind that check through the contributor.

A process-specific dependency can join through HealthReadinessContributorBackendService, injected under SAAS_SERVICE_TYPES.HealthReadinessContributorService. Defining an interface implementation alone does not register it: the process container must bind that contributor. Inspect its actual registration when extending readiness.

A sustained failure is logged again when the failing set changes, or at the throttled interval with suppressed-probe information. Readiness reports a condition; deployment routing, alert policy and repair remain operating decisions.

Notice when an organization has no usable owner Guarantee

An organization can lose administrative access through imported data or an out-of-band change, even when normal application actions protect the last owner. Wildo checks the administrative population at startup to make that state visible.

Missing usable owners are reported as errors; a single usable owner receives a warning. A separate application-wide check reports a missing usable super-administrator; its healthy single-administrator case is not the organization-owner warning. These observations help operators act before someone discovers the lockout through a failed task.

Example: Detect a stranded organization after a restore

Restored records leave an organization without an active person holding usable ownership. On startup, the backend reports the organization and records breach evidence when its audit service is available. The operator follows the recovery direction; other organizations can continue running.

A startup report distinguishes missing, single and multiple administrative owners.
For engineers
Read the population under the correct authority

The report resolves roles that confer ownership and checks membership and person status. Its measurement creates a system execution context through the authenticated context factory: a tenant-confined read would be incapable of describing the whole estate.

This excerpt is from startup’s organization-owner report. The arguments are runtime services already resolved by the host; it is not a public endpoint or an instruction to grant a caller system access.

const population = await measureOrganizationsMissingUsableOwner({
  authExecutionContextFactory,
  repositoriesRegistry: this.repositoriesRegistryService,
  errorBuilder: this.errorBuilder,
  conferringRoles: resolveOrganizationOwnerConferringRoles(this.authorizationsService),
});

The observation distinguishes organizations with no usable owner from those with exactly one. The super-administrator report measures the application-level population separately. The backend runtime profile owns these reports; confined worker profiles skip populations outside their authority.

Follow a breach into evidence

The organization report now calls the audit service for each stranded organization. This actual call records how the breach was detected; individual audit failures are isolated so the diagnostic does not prevent startup.

await this.auditLogsService?.logOrganizationOwnerFloorBreached({
  organizationId: stranded.organizationId,
  detectedBy: AdministrativeContinuityBreachDetection.BOOT_REPORT,
  remainingUsableOwners: 0,
});
PopulationOperator meaning
No usable ownerAdministrative continuity is broken; recovery requires an authorized external path
Exactly oneAccess works, but adding another owner reduces fragility
More than oneThis measurement has not found the missing-owner condition
Keep diagnosis separate from repair

The startup report does not appoint an owner or fail readiness. Application-wide loss has no higher in-product scope from which to grant authority, so recovery is an operator procedure. Monitoring the log and available audit stream, and exercising that procedure, are part of operating the application. The report supplements the write guards; it is not a continuous replacement for them.

Handle access and lifecycle changes

Give platform services their own connections Guarantee

Platform services need access to the services they use, without each receiving the operator’s administrative credentials. The applications manager provisions their connection material and makes the registered services’ configuration available through the trusted platform control plane.

Object storage uses short-lived sessions confined to the service’s platform identity. The provisioner keeps the administrative authority needed to create access; the consuming service receives the connection or session intended for its work.

Example: Prepare storage access for a platform service

An authenticated platform service requests storage access. The manager ensures the bucket exists, then issues a session constrained to that service’s platform application identifier. The response includes the expiry and identifier needed by the storage client, without returning the object store’s root credential.

The platform manager service supplies separate service credentials and time-bound storage access.
For engineers
Register the consumer before it asks for access

The platform infrastructure manager provisions the explicit PLATFORM_SERVICES population. Its type coverage checks require services to be listed or explicitly excluded. Provisioning establishes the platform application record atomically, then prepares the applicable Redis, RabbitMQ and signing-key material.

The platform-service retrieval route checks the shared PLATFORM_APPLICATION_PRIMARY_SECRET, then resolves the registered service named by the SERVICE_ID header. This authenticates membership in the trusted platform control plane; it does not independently bind a holder of that shared secret to one service identifier. The signed route through which platform services access application configuration has a different authentication contract. Provisioner administrative connections and boot signing-key inputs must already exist; provisioning does not create its own root authority from nothing.

Issue storage access from the registered service record

The following actual controller excerpt runs after authentication and bucket preparation. storage is the manager’s resolved storage configuration; platformApplication is the registered service selected after the shared-secret check. The root values remain inside the manager’s call to the broker.

const credentials = await this.objectStorageStsBroker.issueApplicationScopedCredentials({
  stsEndpoint: storage.stsEndpoint,
  rootAccessKey: storage.accessKey,
  rootSecretKey: storage.secretKey,
  bucket: storage.bucket,
  applicationId: String(platformApplication._id),
  durationSeconds: storage.sessionDurationSeconds,
});

The confinement identifier comes from that selected record, never a caller-supplied storage prefix. Prefix confinement of the issued session and authentication of the caller are separate boundaries. The manager ensures bucket existence with administrative rights first, because the confined session is intentionally unable to create the bucket.

Distinguish the credentials’ lifecycles
MaterialOwner and lifecycle
Provisioner administrative connectionsOperator-supplied authority held by the manager
Service connection materialProvisioned and persisted for the registered service
Signing keysService-specific identity material, including required boot inputs on applicable services
Storage sessionBroker-issued, prefix-confined and returned with expiresAt

The storage response also carries applicationId so the consumer builds keys from the issuer’s decision. It must renew expiring sessions through its credential flow rather than widen storage permissions. Session expiry is not scheduled rotation of every longer-lived service credential.

A provisioning result and a ready consumer are different observations. Check the authenticated retrieval path and the consuming runtime’s storage readiness together when diagnosing connection failures. The storage credential end-to-end lane exercises both that success path and refusal of unauthenticated or unregistered callers.

Replace credentials without an abrupt cutover Feature

Integrations need time to adopt a replacement secret. Wildo can keep the previous API key or OAuth client secret valid during a declared overlap window, then reject it after the deadline.

Rotation, regeneration and revocation have different purposes. A timed rotation supports a planned handover; revocation stops access. The authentication path checks the credential’s status and expiry as well as the previous secret’s window.

Example: Move an integration to its replacement key

An administrator rotates a credential with a handover deadline. The integration is updated while both secrets are accepted. After the deadline, requests using the previous secret fail while the new secret continues to work, provided the credential remains active and unexpired.

Old and new credentials overlap until the previous credential reaches its deadline.
For engineers
Choose the right administration operation
OperationMeaning for the previous secret
RotateA declared invalidation date bounds its acceptance
RegenerateOpen-ended overlap until the next reissue
RevokeThe inactive status prevents authentication, including the previous secret
Extend expiryChanges the credential lifetime; it is distinct from the previous-secret deadline

Regeneration is not a response to a compromised secret. Choose revocation or a rotation deadline appropriate to the incident, and arrange how the legitimate consumer obtains its replacement through the authorized credential flow.

Perform a deliberate API-key handover

For an organization API key, use its declared ROTATE operation as an authorized organization administrator (or the administrator variant). This operation requires step-up authentication. The request and response fields below are selected from that operation’s schema:

requestDto: z.object({
  oldKeyInvalidationDate: z.date().min(new Date(), 'Invalidation date must be in the future or now').default(() => new Date()),
}),
customResponseDto: ApiKeyOrganizationSchema.extend({
  plainKey: z.string().min(1).isEphemeral(),
  oldKeyId: z.string(),
  oldKeyInvalidationDate: z.date(),
}),

Supply an explicit future oldKeyInvalidationDate when invoking the operation through the application’s administration interface or generated client. Omitting it means immediate cutover, not a default handover period. The schema fragment is a contract excerpt, not code to copy into an application or a guessed endpoint URL.

StepWhat to do and observe
PrepareSelect an active, unexpired key, choose a deadline and complete the required step-up
RotateSubmit the chosen deadline and capture the response’s one-time plainKey into the consumer’s secret store
SwitchUpdate the integration; verify the new secret works and the previous secret still works before the deadline
FinishVerify the previous secret is rejected after the deadline while the new one remains accepted

There is one previous-secret slot. Rotating again replaces that slot even if its earlier deadline has not passed. Finish one handover before beginning the next; never assume a chain of older secrets remains accepted. The returned plaintext is ephemeral and must not be logged or treated as a field that can be retrieved later.

This workflow concerns API-key records. Framework-managed platform secrets use coordinated provisioning and distribution, not this record’s overlap window.

Keep the two clocks distinct

These are the actual shared predicates used by machine authentication. The first checks the previous-secret slot; the second checks the lifetime of the credential itself.

export function isRotationGraceWindowOpen(invalidationDate?: Date | null): boolean {
  if (invalidationDate === null || invalidationDate === undefined) return true;
  return new Date(invalidationDate).getTime() > Date.now();
}

export function isCredentialExpired(expiresAt?: Date | null): boolean {
  if (expiresAt === null || expiresAt === undefined) return false;
  return new Date(expiresAt).getTime() <= Date.now();
}

The callers also require an active record. An open overlap cannot revive a revoked credential, and a delayed administrative status update cannot extend an expired credential’s actual access.

Follow persistence and authentication together

Reissue handlers write the previous-secret and grace fields through the repository’s server-managed field allow-list. The API-key request handlers, OAuth client handlers and OAuth token exchange use the shared date rules. The integration test for rotation persistence specifically checks that strict schema parsing preserves authorized injected fields.

A nightly expiry sweep now updates expired records through their expire operation. That bookkeeping can lag the clock, so authentication still checks expiresAt on every use. For a handover check, exercise both secrets before the deadline and the old secret after it; also check revocation independently. A single successful request with the new key does not establish the overlap behavior.

Finish active work before stopping Guarantee

A release or scale-down should give work already underway a chance to finish. Wildo coordinates stopping job intake, draining active jobs and running cleanup through one shutdown sequence.

Workers and telemetry participate in the same lifecycle. The drain has a deadline, so the deployment and the application’s job behavior still need to agree on how interrupted work is retried.

Example: Let a worker finish its current deliveries

During a deployment, a worker stops accepting new jobs while its current deliveries complete. Once they finish, registered cleanup can flush telemetry and close connections. If the drain deadline expires, the process proceeds with shutdown and reports the outstanding jobs.

The intended shutdown sequence: stop intake, allow active work to drain, attempt signal flushing, then exit.
For engineers
Separate stopping intake from releasing resources

WorkerShutdownManagerBackendService exposes two registration points: registerStopHandler runs before draining; onShutdown runs afterward, in reverse registration order. Registering signal handlers is idempotent, and cleanup is protected against double execution across graceful and forced paths.

The following is an illustrative integration inside a custom consumer that already receives the shutdown manager and owns stopConsuming, closeConnections and processJob. These methods represent the application’s existing work; they are not additional Wildo APIs.

shutdownManager.registerStopHandler(() => stopConsuming());
shutdownManager.onShutdown(() => closeConnections());

async function runAcceptedJob() {
  shutdownManager.jobStarted();
  try {
    await processJob();
  } finally {
    shutdownManager.jobCompleted();
  }
}

The finally is essential: failed work must release the counter too. Stop handlers must prevent further admission; cleanup must not close a connection while accepted jobs still need it. Built-in consumers already integrate with the manager; do not count their jobs a second time.

Size the operating window around the work

The manager’s default drain period is 25 seconds. The actual process termination allowance must leave time for stop handlers and cleanup as well as the drain. A long-running job needs interruption and retry semantics appropriate to its side effects; a grace period does not make delivery exactly once.

StageResponsibility
Stop handlersStop accepting additional work
In-flight counterTrack accepted work through success or failure
Drain deadlineBound how long shutdown waits for that work
Shutdown handlersRelease resources and flush registered telemetry
Broker closure and exitFinish the process lifecycle

Telemetry registers its flush with this coordinator rather than owning a competing termination path. A hard kill cannot run that sequence. HTTP requests are not automatically included in the job counter: verify the HTTP host’s own lifecycle and the deployed termination policy separately.

Keep work bounded

Set request limits where the cost begins Mechanism

A sign-in attempt, message or expensive operation needs a request budget suited to its purpose. Wildo supports declared time windows, enforced through shared counters and keyed by the address or authenticated subject appropriate to the endpoint.

HTTP callers receive quota and retry information. The policy belongs to the operation’s owner: the framework supplies enforcement, while you decide what usage is reasonable and which identity should share a budget.

Example: Limit repeated requests for one account

A controller resolves the authenticated account and uses that identity for its request budget. Requests from different addresses then contribute to the same account bucket, instead of gaining another allowance simply by changing address.

A request window admits work within its limit and refuses excess requests.
For engineers
Use a policy, not a separate counter implementation

Resource variants can declare rateLimit. Custom HTTP controllers call checkPolicy, and headerless authenticated flows can use checkPolicyForSubject. These share the policy-window evaluator.

This illustrative custom-controller call assumes rateLimitService is injected, req and res are the current Express request/response, and authenticatedUserId and organizationId come from verified server context. The authored values describe a sample policy, not a global default.

await rateLimitService.checkPolicy(
  req,
  res,
  { requestsPerMinute: 10, requestsPerHour: 100 },
  'example:account-action',
  organizationId,
  authenticatedUserId,
);

The final argument replaces the default address-derived identity. Never take that value from an untrusted request field. The namespace separates this action from other policies; organization scope separates tenant counters.

Carry the declaration across execution planes

For an existing API-call resource variant, an illustrative policy fragment is:

rateLimit: {
  requestsPerMinute: 10,
  requestsPerHour: 100,
},
PlaneBudget identity and scope
REST resource operationAddress-derived identity in the HTTP operation namespace
MCP resource toolResolved credential/user subject in an MCP-specific namespace
Agent resource toolResolved credential/user subject in an agent-tool namespace

These are independent budgets, not one shared allowance across transports. Supported MCP authentication supplies a credential or user identity, which remains attached to the operation context and determines the counted subject. The declared operation policy is checked before dispatch. Internal contexts must preserve that identity contract; a defensive missing-identity branch is not a supported client configuration.

Interpret multiple windows together

The evaluator increments every configured window. The HTTP response preserves the tightest remaining quota across applicable checks. When several windows are exceeded, retry guidance reflects the latest reset needed to satisfy all of them.

SurfaceAppropriate use
Resource variant rateLimitPolicy consumed by REST, MCP and agent-tool execution, with separate buckets
checkPolicyCustom controller with HTTP quota headers
checkPolicyForSubjectAuthenticated flow without an HTTP response object
Low-level counterSpecialized callers whose semantics require direct counter handling
Understand the operating behavior

These are fixed windows over shared atomic counters, not a token bucket. A burst at a boundary can use the end of one allowance and the start of the next. Per-endpoint enforcement propagates a shared-counter failure; the broader HTTP perimeter limiter has a separate in-memory fallback policy.

Non-production rate maxima are relaxed through the shared environment policy while the real window counter remains in use. A local run therefore does not demonstrate production ceilings. Verify the actual execution plane calls the relevant policy method: an HTTP declaration alone is not proof that every alternate transport enforces it.

Keep record pages within their declared size Guarantee

A client should not turn a normal record listing into an entire collection download by asking for an enormous page. Wildo applies the operation’s page-size ceiling in its repository adapters.

The response reports the page size actually used. You choose a ceiling suitable for the operation; filters, counts and internal whole-collection work need their own performance decisions.

Example: Keep a listing manageable

A listing allows pages of 50 records. A client asks for 10,000. The paginated listing applies the declared ceiling and returns pagination metadata reflecting that limit, so the client can continue through pages instead of receiving an oversized response.

A large set of records is served through a bounded page.
For engineers
Declare the ceiling on the operation

maxPaginatedResultPerPageLimit belongs to the operation configuration. Both MongoDB and PostgreSQL paginated list adapters read it, with 100 as the fallback ceiling and 20 as the ordinary default page size. The external HTTP API repository also applies a paginated-read clamp.

The user-profile resource supplies a concrete authoring example. This is its default search variant, extracted from the resource’s operations map; the separate administrator variant is omitted. The operation, variant, role and risk enums are public model exports from @wildo-ai/saas-models.

[CoreResourceOperation.SEARCH]: {
  variants: [{
    variantType: ResourceOperationVariantType.API_CALL,
    isDefault: true,
    roles: [CORE_APP_ROLES.APP_USER],
    riskLevel: ResourceOperationRiskLevel.LOW,
    isSearchable: true,
    searchableFields: ['firstName', 'lastName', 'displayName'],
    searchableOptions: { caseSensitive: false, fullMatchOnly: false },
    filterFields: {},
    sortFields: ['firstName', 'lastName'],
    maxPaginatedResultPerPageLimit: 50,
  }],
},

The ceiling sits beside the fields and access rule for the selected variant. This resource separately allows a larger ceiling for its administrator search variant: a different access contract can have a different page budget. The request schema and repository consume the selected variant’s contract.

Follow the declared ceiling into execution

This excerpt shows the list adapter’s effective limit computation. operation is the resolved operation configuration and listOptions contains the caller’s requested pagination.

const maxLimit =
  (operation as { maxPaginatedResultPerPageLimit?: number }).maxPaginatedResultPerPageLimit || 100;

limit = Math.min(maxLimit, Math.max(1, listOptions?.limit || 20));

The lower bound is one; the upper bound comes from the operation. The list result’s pagination metadata reports the applied limit rather than echoing an impossible request size. Choose that declaration alongside the columns returned and the expected record size.

Distinguish the read contracts
Read pathHow to reason about the bound
Paginated listThe repository clamps the effective page size
SearchThe request schema applies the configured maximum; repository search also bounds its page
Internal MongoDB/PostgreSQL non-paginated enumerationPreserves its whole-result contract
HTTP API read-through, including internal non-paginated readsRemains capped at the remote adapter’s maximum; one response does not establish complete enumeration

Search uses its own default maximum when no limit is configured; do not infer its fallback from list behavior. The adapter parity tests pin the list ceiling across MongoDB and PostgreSQL, and the hostile HTTP tests exercise query coercion and response metadata.

For a remote dataset, follow the remote service’s supported pagination contract and verify traversal completion. Do not treat an internal non-paginated call as an instruction to collect every remote record.

Budget work beyond the returned records

A bounded response can still involve expensive filtering and counting. Authorization remains independent from pagination, and an internal export that requires every record must implement appropriate traversal rather than rely on the public page guard. Pair the page ceiling with request policies when the operation also needs a frequency bound.

A working environment is more than a collection of services.

The service choices, runtime access and operating signals need to agree. Wildo carries those relationships through configuration, deployment and the running application.

You choose where the services run and how they are operated. The shared mechanisms make those decisions explicit and easier to follow.

Building a B2B product or an internal tool?

Wildo is not self-service yet. Tell us what you have in mind and we will say plainly whether it fits, and what happens next.