Skip to main content
Wildo.ai Coming soon

Development to production

Configure local and remote environments around the same application and service definitions.

Development · staging · production · environment-specific connections

> Develop on your machine > Prepare a remote environment > Keep the application’s structure shared

An environment is the setup around your application: where its processes run, which services they use and how they are reached.

Wildo keeps the application’s definitions shared while giving each environment its own connections, credentials and deployment choices.

Develop locally, prepare staging and production, and choose when each is ready to serve.

One application definition feeds separate local and remote environments, each with its own access keys, services and data.

Keep the application recognizable as its surroundings change

Share the application, vary the setup

Resources, service roles and business behavior belong to the application. Addresses, backing services and deployment settings belong to its environments. Change the setup from an authored definition with one place to review those choices.

Give each environment its own access

A developer’s local database and the production database serve the same role with different connections and credentials. Wildo connects the selected environment to the access each application process needs.

Carry a release into its destination

Container definitions and deployment workflows connect the application to its target. Check the generated result, then release it with the destination’s storage, routing and operating requirements in place.

Example: Prepare production while development continues

A team develops a records-and-attachments application on a laptop. Production uses its own database, file storage and public domain. The application’s service definitions are shared; the production connections and credentials are prepared separately. Local test records stay local unless the team deliberately transfers them.

For engineers

Keep the decisions at their authored homes

Application services are declared in wildo.saas.config.ts. Each environment has a discovered infrastructure definition, and its secret configuration supplies the authority used during setup. Generated process configuration and deployment files are outputs of those inputs.

DecisionShared application basisEnvironment-specific choice
ServicesDeclared backend, frontend, worker and other service rolesWhich processes run and how they are deployed
Data accessResource definitions and repository behaviorCompatible database connection, credentials and existing data
Files and background workThe application’s storage and queue needsSupported providers, namespaces and service addresses
Public accessDeclared service keysDomain, service hostnames, ingress and certificate setup
ReleaseApplication source and container build definitionsImage registry, deployment target and release timing

An environment choice carries configuration into its consumers. It does not make incompatible database behavior interchangeable, copy business records or bring a remote service online by itself.

Choose location and runtime independently

locationType describes local or remote operation. runtime selects Docker Compose or Kubernetes. runtimeEnvironment supplies the application’s runtime mode. These fields answer different questions: local Kubernetes uses Kind, while remote deployment can use Compose or Kubernetes.

The following excerpts follow the existing application environment layout. They show the contrast in location, addressing and a self-hosted PostgreSQL connection; other backing services, secrets and release settings are intentionally left out. They are not complete deployment recipes.

In infrastructure/local/wildo.infra.local.config.ts:

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

export default defineInfraEnvConfig({
  environment: WildoEnvironment.LOCAL,
  locationType: WildoLocationType.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: 'application-local',
    },
  },
});

In infrastructure/production/wildo.infra.remote.config.ts, a remote Kubernetes example uses explicit deployment metadata and neutral operator-selected values:

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

export default defineInfraEnvConfig({
  environment: WildoEnvironment.PRODUCTION,
  locationType: WildoLocationType.REMOTE,
  runtime: WildoDeploymentRuntime.KUBERNETES,
  runtimeEnvironment: RuntimeEnvironment.PRODUCTION,
  publicDomain: 'example.com',
  database: { engine: DatabaseEngine.POSTGRESQL, source: BackingServiceSource.SELF },
  applicationDatabase: { engine: DatabaseEngine.POSTGRESQL },
  backingServices: {
    postgresql: {
      source: BackingServiceSource.SELF,
      host: 'postgres.internal', port: 5432, database: 'application-production',
    },
  },
  serviceHosts: { app: 'app', docs: 'docs', website: '' },
  remoteProvider: {
    provider: 'scaleway', registryEndpoint: 'rg.fr-par.scw.cloud/example',
  },
  kubernetes: {
    namespace: 'application-production',
    ingressClass: 'traefik',
  },
});

The database names and postgres.internal address are illustrative: supply the connection reachable from the target. database selects platform persistence, while applicationDatabase selects the application’s default store. Neither transfers data.

The service keys must match the application. Here the application uses app.example.com, documentation uses docs.example.com, and the website uses the apex domain. The same resolved hostnames feed advertised endpoints and generated routes. A frontend assigned to a CDN keeps its public address but follows its separate publication and TLS setup.

Remote Kubernetes rendering needs explicit namespace, public domain, image registry and runtime mode. This example supplies the image registry through remoteProvider.registryEndpoint; the release configuration must publish the referenced images there. The Docker or Kubernetes page explains the runtime-specific deployment shape.

Choose backing services separately from the application host

A remote application can use self-hosted services, vendor-managed services or a supported mixture. A service’s hosting choice is separate from whether the application runs locally or remotely. Check both the resolved provider and its executable provisioning support: a recognized provider name alone does not mean Wildo creates that vendor’s infrastructure.

Keep operator authority separate from application runtime credentials. Provisioning can require broad access to create service users or namespaces; a running application receives its own configured access. Refresh each environment through its normal registration and configuration flow rather than copying a developer’s process environment into production.

The managed backing services page follows provider selection, scoped credentials and operating signals in detail.

Use the lifecycle for the selected destination

Local Compose development runs through wildo local dev, which prepares supporting infrastructure and supervises application development. It refuses remote environments and does not orchestrate local Kubernetes. Remote deployment instead consumes the generated image and deployment workflow for that environment.

For an already configured and registered production Kubernetes environment, prepare and check its deployment inputs from the application workspace:

# Generate the selected environment's configuration and deployment inputs.
wildo config sync --env production --domain config

# Check sandbox manifests and the saved application deployment YAML.
wildo config validate --env production --domain manifest --deployment-artifacts

Read synchronization’s writes and skips. Platform access and application registration must be available for application deployment generation. The validation command checks infrastructure/platform sandbox output and the saved application manifests; it does not apply them to the cluster. Missing application output is a failure, not evidence that there was nothing to deploy.

The generated release workflow has separate inputs for registry authentication and target access. Review those with the emitted images, namespaces, secrets and routes before running the release. Development-process health and successful YAML validation establish different facts from a working remote application.

Keep platform and application authority distinct

The platform manages application registration, configuration delivery and shared operating services. Each target needs its own valid registration and runtime access; registration does not deploy the application’s workloads.

Sharing a host or platform does not merge application users, customer organizations or their access policies. Deployment location describes where code runs. The application’s declared authorization rules determine who can act on its records. Regenerating configuration also does not refresh already-running processes: restart or roll out the affected services through their normal lifecycle.

Treat moving data as its own operation

Preparing another environment gives the application another place to run. Decide separately whether that destination starts empty, receives a controlled copy or takes over an existing service.

Before switching trafficWhat must be established
DatabaseDestination connection, shipped schema migrations and the intended records
AttachmentsFile objects and their references remain consistent in the destination
Public endpointsDNS, certificates and service routing reach the intended deployment
Runtime accessEach process can authenticate to the services it uses
Release and recoveryThe application works on the target, with a deliberate cutover and recovery procedure

Local persistence preserves development state across container replacement; it is not replication or backup. Changing a provider, runtime or environment does not perform the transfer in this table. Shared declarations keep the configuration connected while the deployment and data checks establish the operating result.

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.

Prepare what will actually run

A release brings together application configuration, dependencies, container images and browser policies. Wildo derives these connected inputs from their declarations, so moving beyond development does not start with a second description of the application.

Generated workflows make the steps reviewable. Your team chooses the release checks, supplies deployment credentials and verifies the application in its destination environment.

Release preparation connects packaging, checks and delivery to a running server.

Carry the application into its release

Package the declared dependencies

Keep application version pins aligned with the framework. Platform images install their dependency graph inside the target system, including its native libraries.

Generate for the destination

Prepare workflows and browser policies from environment configuration. Keep the deployment branch, service images and allowed browser connections tied to the environment they serve.

Check before relying on it

Use configuration and artifact checks to catch specific mismatches. Complete those checks with the runtime and user journeys that establish whether a release works.

Example: Prepare a staging release

A team gives staging its own API origin and deployment branch. It refreshes deployment inputs and browser policies, reviews the workflow and builds the service images. After rollout, it checks the served policy and the application’s main journeys before promoting the release.

For engineers

Which work belongs to the application?

Application release preparation consumes environment and service declarations. Framework image publication and repository-wide architectural checks have different owners; they support the application without becoming an automatic release gate for it.

SurfaceDerived fromWhat the operator or application team supplies
Deployment workflowEnvironment branch/runtime and declared build targetsCredentials, reviewed custom steps and rollout verification
Browser policy artifactsSurface policy, environment overrides and provider footprintsReporting or enforcement posture, then verification of the delivered header
Application dependenciesPropagated pins and required peersInstallation, lockfile review and application tests
Platform imagesFramework package graph and publication targetsA selected published version and reachable image registry

Prepare a declared environment

The following is an illustrative command sequence from an application root with an authored staging environment. It prepares and checks inputs; image builds and deployment remain visible steps in the reviewed workflow.

# Generate target configuration and reviewable workflows.
wildo config sync --env staging --domain config
wildo config sync --domain cicd

# Compose browser artifacts for the same environment and verify agreement.
wildo dev sync-csp --env staging
wildo dev sync-csp --env staging --check

# Validate the application's configuration.
wildo config validate --env staging

Configuration sync must precede compilation where it generates provider modules into source. Existing workflow files are preserved; adopt changes deliberately after reviewing any operator customization. The generated workflow’s secret mappings need matching credentials in its execution environment.

The Kubernetes template includes manifest application. The Docker Compose template requires the operator’s remote delivery and apply procedure. Neither a generated workflow nor a configuration check establishes that a host is healthy or that a business transaction succeeds.

Verify the artifact that reaches users

A source checkout, a generated artifact and a running deployment answer different questions. Check the image version and architecture actually pulled, the browser policy actually served and the application paths the release must support. Keep those runtime checks alongside the workflow that performs the release.

Package the runtime

Package platform services with their dependencies Tool

Wildo’s platform services share a container image built from their declared dependency graph. Dependencies are installed inside the image, so native libraries are prepared for its operating system and architecture.

The runtime selects which platform service starts. Application services retain their own Dockerfiles; this platform image is not a universal application base image.

Example: Run the registry beside other platform services

An installation can use the same platform image for its application manager and module registry, selecting a different service and supplying its configuration for each container.

Declared packages form a platform image, with app manager, scheduler and module registry selected at runtime.
For engineers
What does the Dockerfile include?

Dockerfile.platform-base uses the framework root as its build context, with a Dockerfile-specific ignore file. It expects compiled dist/ trees to exist before packaging.

Selected production install and peer guard steps from that Dockerfile:

RUN pnpm install --frozen-lockfile --prod \
    --config.node-linker=hoisted \
    --filter "@wildo-ai/platform-apps-manager..." \
    --filter "@wildo-ai/platform-crontabs-batches-manager..." \
    --filter "@wildo-ai/platform-module-registry..."

RUN node templates/infra/docker-images/platform/verify-peers.mjs /app/wildo-ai \
    "@wildo-ai/platform-apps-manager..." \
    "@wildo-ai/platform-crontabs-batches-manager..." \
    "@wildo-ai/platform-module-registry..."

The trailing ... selects each service and its dependency closure. Installation happens inside the Linux builder stage; host-installed native binaries are not copied as the dependency solution. The guard resolves non-optional peers from the selected packages so a missing runtime peer fails during image preparation.

How is a service selected?

The entrypoint reads PLATFORM_SERVICE, including platform_apps_manager, platform_crontabs_batches_manager and platform_module_registry. Its configuration supports JSON values or corresponding file inputs such as INFRA_SECRETS_FILE, letting deployment tooling deliver configuration through mounted files.

This is framework platform packaging. The separate backend base Dockerfiles are not in the current publication set and are not inherited by application Dockerfiles. When extending the published platform image, update its real service selection and dependency filters together; adding a file to the repository does not automatically make it a runnable image service.

Use one platform image reference on Intel and Arm Guarantee

Wildo’s platform-image publication lane prepares Intel/AMD and Arm variants under one versioned reference. When that manifest is published, each compatible host selects the image for its architecture without a different image name in the deployment configuration.

This applies to the framework’s platform image. Application images need their own architecture-aware build setup.

Example: Share a version across different machines

An Arm development machine and an Intel server can reference the same published platform version. Each pulls its corresponding Linux image, including native dependencies prepared for that architecture.

One platform image reference resolves to an Intel/AMD or Arm variant.
For engineers
How are the variants produced?

FrameworkDockerPublishService uses FRAMEWORK_DOCKER_IMAGE_PLATFORMS, containing linux/amd64 and linux/arm64. This selected source excerpt shows the publication invocation after target resolution:

FrameworkDockerPublishService.runDockerCommand('buildx build --push', [
  'buildx',
  'build',
  '--platform',
  FRAMEWORK_DOCKER_IMAGE_PLATFORMS.join(','),
  '-f',
  buildContract.dockerfilePath,
  '-t',
  target.imageRef,
  '--push',
  buildContract.contextPath,
]);

The platform Dockerfile leaves platform selection to this invocation and installs dependencies inside each target’s builder stage. Building a non-native target may need emulation or a suitable remote builder; creating both variants is separate from running either one.

Why are aliases handled separately?

Only after all immutable image pushes succeed does the publisher repoint mutable aliases with docker buildx imagetools create. This references the full manifest rather than retagging whichever single-architecture image a local daemon holds.

The local rebuild path creates an image for the host rather than publishing a multi-platform manifest. Application image workflows are also separate: the platform publisher’s target list does not automatically configure their architecture list.

For a release, inspect the published manifest and exercise the intended hosts. The emitted two-platform command establishes the publication contract; it is not by itself evidence that a particular registry tag contains both runnable variants.

Prepare the release

Start deployment workflows from your environment Tool

An environment already names its deployment branch and runtime. Wildo uses those choices and the application’s declared services to prepare a GitHub Actions workflow, keeping its starting point connected to the application it will ship.

The workflow exposes generation, image builds and rollout as readable steps. Operators supply credentials and hosting details, review generated changes and complete remote delivery for Docker Compose.

Example: Add a worker to the release

When an application declares a worker, workflow generation includes that worker’s own image build alongside the services. Review the newly generated workflow against an existing customized one before adopting it.

An environment declaration feeds generation, image building and an operator-owned rollout.
For engineers
Which declarations drive generation?

This is the deployment block from Wonder Todos’ production infrastructure configuration. Its surrounding environment declaration and imports are omitted:

deployment: {
  branch: 'main',
  runtime: WildoDeploymentRuntime.KUBERNETES,
  namespace: 'wonder-todos-production',
},

CiWorkflowGeneratorService selects the runtime template, resolves the image-tag strategy and derives build targets from declared services, workers and minions. Each target needs its Dockerfile at the configured package path. The example is authored configuration, not evidence that this particular production environment has been deployed.

Generate, inspect and adopt

From an application root, after declaring its environments:

wildo config sync --domain cicd

# Review .github/workflows/deploy-<env>.yml before using it.
# Deliberately replace existing generated workflows only after preserving custom work:
wildo config sync --domain cicd --force

Existing files are preserved by default. --force replaces them; it does not merge operator edits. Re-running generation after changing a service therefore requires deliberate adoption of the new workflow.

RuntimeGenerated procedureOperator setup
KubernetesGenerate artifacts, build and push service images, install ingress/TLS controllers and apply manifestsRegistry credentials, cluster credentials, names and reachable infrastructure
Docker ComposeGenerate deployment inputs, build images and push them when the environment declares a registry endpoint; remote deployment is provided as commented guidanceRegistry credentials, remote transfer, target-host apply and verification

Both templates run npx wildo config sync --ci --env=<environment> --domain config before image builds, because provider artifacts generated into package source must exist before compilation. They then sync each frontend’s Content-Security-Policy for that environment, since an image bakes its policy in at build time. Images are pushed to the environment’s remoteProvider.registryEndpoint — the registry the generated manifests pull from — logging in with REGISTRY_USERNAME and REGISTRY_TOKEN repository secrets, or the workflow’s own GitHub token for GHCR. A Docker Compose environment without an endpoint builds images and says in the workflow that nothing is published. The Kubernetes template later applies .wildo-saas/deploy/<environment>/k8s/.

Generated workflows target GitHub Actions. Secret mappings name variables configuration sync consumes; storing and granting those secrets remains an operator responsibility. A successful workflow generation is a reviewable procedure, not a completed remote rollout.

Ship the browser policy you configured Tool

A browser security policy must reach the server that actually serves the application. Wildo resolves each surface’s policy for the selected environment and writes both its generated application artifact and the marked policy region in its web-server configuration.

Check mode detects drift without rewriting files. You choose whether a policy reports violations or enforces restrictions, and verify the delivered header in the deployed environment.

Example: Release with the right API origin

A remote application calls a different API address from local development. Generate the browser policy for that environment before packaging the surface, so its connection rules and server header describe the intended destination.

One browser policy is rendered into an application configuration artifact and a web-server policy.
For engineers
Resolve an explicit environment

From an application root, with a declared production environment and an app frontend service, this command sequence first reports drift, then refreshes and verifies the artifacts:

wildo dev sync-csp --env production --service app --check

# After reviewing a reported difference:
wildo dev sync-csp --env production --service app
wildo dev sync-csp --env production --service app --check

--check performs the same resolution without writes and exits nonzero for stale output. Omit --service to cover the CSP-capable frontends. Environment resolution uses --env, then WILDO_INFRA_ENV, then the active environment; production execution refuses an implicit local fallback.

What is composed and written?

The resolver combines the surface baseline, application policy, environment overrides and declared provider footprints. Provider origins are service-specific and come from materialized provider data; run configuration synchronization after changing provider declarations.

OutputConsumerRelease responsibility
src/generated/csp.generated.jsonThe surface’s CSP configuration adapterGenerate it for the target environment before packaging
Marked nginx policy regionsThe web server serving the built surfaceCopy the updated configuration into the runtime image

The writer preserves nginx text outside the policy markers. It rejects invalid service policy keys and missing required API connection origins before emitting the artifact set. Development startup also invokes synchronization.

Reporting and enforcement are different choices

For the SaaS application surface, an omitted policy defaults to enabled, report-only behavior. An environment named production does not itself turn enforcement on. Inspect reportOnly and the emitted header name: Content-Security-Policy-Report-Only reports; Content-Security-Policy enforces.

Generated files and check mode establish agreement with current configuration. Inspect the actual response header and browser behavior after deployment to establish what users receive; a correct source artifact alone cannot prove the running server uses it.

Check consistency

Keep dependency versions aligned Tool

An application has its own workspace and lockfile. Wildo carries the framework’s relevant dependency pins into that workspace and declares required peers, reducing accidental version differences between the framework and its applications.

The alignment tools update manifests. Installing dependencies makes those declarations effective; application-owned dependencies and intentionally separate toolchain choices retain their own ownership.

Example: Add a package without losing its peers

A newly composed package uses an engine library that requires peer dependencies. The delivered peer catalogue tells composition which declarations to add, while the application’s shared version overrides select their versions.

Framework version declarations feed application pins and required peer declarations before installation.
For engineers
Which authority determines the versions?

The framework root’s pnpm.overrides supplies version pins. app-dependency-alignment.mjs derives the propagated names from framework manifests, applies explicit exclusions, and reconciles application overrides. The scaffolder consumes that derived set for new standalone applications.

For a framework contributor maintaining the checked-out Wonder Todos application, these are the alignment script’s supported commands. Run them from the framework root; this is not a standalone-application CLI recipe:

node scripts/app-dependency-alignment.mjs --app examples/wonder-todos --check

# Apply manifest changes, then verify the declarations:
node scripts/app-dependency-alignment.mjs --app examples/wonder-todos --write
node scripts/app-dependency-alignment.mjs --app examples/wonder-todos --check

# Refresh the application's own installation and lockfile:
pnpm --dir examples/wonder-todos install

Review the manifest and lockfile changes together. --write does not install packages, and an override cannot supply a peer that no consumer has declared.

Why track peers separately?

engine-peer-closure.mjs computes transitive required peers. The closure is delivered as engine-peer-closure.json, so composition can add a package after scaffolding without a framework checkout. Required peers are declared with wildcard ranges; the shared override remains the version authority. Optional peers are not treated as unconditional requirements.

ConcernSource of the answer
Which version?The relevant framework pin, propagated into application overrides
Which required peers?The engine dependency closure for the consuming package
What is installed?The application’s installation and lockfile

Reconciliation does not remove declarations merely because they no longer appear in the derived closure: an application may own them independently. Framework-development compiler, watcher and finalizer choices are deliberately outside application pin propagation.

Check the rules that hold the framework together Guarantee

Wildo’s own repository runs targeted checks for rules that ordinary typechecking cannot express: package boundaries, dependency coherence and unsafe patterns, among others. These checks help protect the common foundation that applications inherit.

Each workflow defines which changes trigger it. Application teams add their own behavior tests and release checks; a framework check does not establish that an application works correctly.

Example: Keep a database test portable

A test that reads MongoDB directly can silently exclude PostgreSQL applications. The store-neutrality checker identifies undeclared database-specific access, so an author uses the shared adapter or explicitly explains why that test targets one database.

Code changes pass through structure, version and secret checks before human review.
For engineers
What runs, and where?

These are framework-repository workflows under .github/workflows/, not a deployment gate automatically installed in every application. Their path filters select relevant changes; each checker owns the property it recognizes. Application scaffolding separately installs wildo-config-validation.yml for application configuration.

The following selected steps are copied from .github/workflows/store-neutrality.yml:

- name: Validate the checker itself (and that no baseline row has gone stale)
  run: pnpm run check:store-neutrality -- --self-test

- name: Validate no e2e store access is store-coupled without a declaration
  run: pnpm run check:store-neutrality

The first step challenges the scanner with fixtures and verifies its baseline. The second scans the actual checkout. --self-test selects a different execution mode: it does not also run the repository check.

What does a passing result establish?
CheckWhat it examinesWhat still needs a separate decision or test
Store neutralityRecognized direct store access and declared exceptionsWhether a scenario correctly tests both stores
Dependency alignmentDerived version pins and required peer declarationsWhether the installed application behaves correctly
Secret scanningSuspected exposed credentials in scanned contentSecret rotation and operational access control

Secret scanning treats changed content as untrusted scan data and obtains scanner controls separately. Its pinned scanner and locally recorded checksums are managed by run-gitleaks.mjs.

Use the workflow log to distinguish a checker failure from an application failure. The purpose is to make recurring architectural mistakes visible while keeping behavior verification explicit.

Coordinate operation. Keep authority explicit.

Shared platform services prepare applications, supply startup configuration and coordinate scheduled work. Each application keeps its own identity and the responsibilities of its runtimes.

Inside an application, product administrators and customer organizations have separate authority. Support access crosses a customer boundary through declared actions and usable grants, with records explaining the access and authority changes.

Shared platform services coordinate applications; customer access remains scoped inside each application.

Follow the responsibility from setup to support

Prepare once, start with the right configuration

Register each application with its own identity and service declarations. Its runtimes receive the configuration intended for them; managed-setting changes are consumed on their next start.

Share the clock, keep execution with the application

The platform dispatches scheduled work to the intended application or minion. The receiving runtime performs it, so dispatch and business completion remain separate outcomes.

Keep customer authority inside each product

Shared hosting does not grant access to another product’s customers. Support uses declared actions and usable grants, with crossing and authority-change evidence explaining the work.

Example: Add a product, then support one of its customers

A team registers a CRM beside its existing task product. Both use shared operational services. Later, a CRM product administrator obtains appropriate customer access and reactivates a suspended CRM account. The action and authority remain within the CRM; shared hosting does not grant access to the task product’s customers.

For engineers
LayerIdentity or scopeWhat it controls
Platform environmentExplicit environment identifierThe shared operating context
Registered applicationCanonical application ID and managed recordConfiguration, provisioning and scheduled-work routing
RuntimeApplication-owned service or runtime principalThe process’s connections and permitted work
Product administrationApplication-scope role within one productThat application’s administrative operations
Customer accountOrganization membership and unit assignmentsWork within the customer’s scope
Temporary support accessTenant or application-wide grantA specifically admitted crossing or read

The operational application manager is not the application-wide super-administrator role. One is a service in Wildo’s platform; the other is authority resolved inside a hosted application’s resource system.

Carry setup through to startup

Register the application before asking its runtimes to load managed configuration. The registration workflow prepares one application snapshot and derives its runtime projections. The loader starts with environment-owned identity and connections, then authenticates separate configuration and secret requests.

The CLI coordinates application migrations and seeding where required. Check the explicit seeding result and actual bootstrap exchange; neither the existence of a platform record nor a generated environment file establishes that the workload is deployed.

Application-wide read approval also needs a different application super-administrator. Prepare that identity through the registration workflow; a first administrator alone does not make that approval path usable.

Keep shared scheduling attributable

An application’s startup synchronizes its declared scheduled work. The shared scheduler delivers signed ticks to the intended application’s queues, with a distinct route for minions. Scheduler replicas coordinate through a lease; the application owns execution and idempotency. A PUBLISHED record establishes dispatch, not business completion.

Evaluate support access before interpreting its evidence

An admitted operation combines the caller’s role, operation declaration and usable grant. Tenant and application-wide grants have different scopes. The admission flag is not a grant, and the crossing observation is not the authorization decision.

For an ownership-repair example, follow declared support actions. For a review of who changed membership, follow authority-change evidence. Both use existing engine mechanisms; application authors should preserve those contracts when adding their own administrative behavior.

Best-effort observation does not make the audit store and business transaction atomic. Keep the destination and loss diagnostics operational, and examine the grant lifecycle alongside the recorded action.

Prepare and operate applications

Share the services around your applications Guarantee

Applications can share the services that register them, coordinate scheduled work and distribute reusable modules. Each application keeps its own identity and configuration while using the same operational foundation.

Wildo packages these services for the supported deployment workflows, with initialization and health checks built into their startup.

Example: Operate a task product and a CRM together

Both applications use the application manager and scheduler. Each registers its own configuration and jobs; shared module infrastructure supports reuse. Operating these common services does not merge the applications’ customer accounts or grant universal access to their records.

Registration, configuration and scheduling services serve two distinct applications.
For engineers
ServiceOwnsDoes not replace
Application managerApplication registration, managed configuration and credential provisioningApplication deployment and business logic
Crontabs and batches managerSchedule records and signed dispatchThe application’s job execution and outcome
Module registryShared module publication and retrieval infrastructureApplication-specific composition and behavior
Initialization jobOne-time platform setup for the environmentA continuously running application service

The deployment templates use a shared platform image with a runtime service discriminator. Compose and Kubernetes describe the platform services and their initialization dependencies. In framework development, services can run from source. These are execution choices around the same named responsibilities.

Platform services use the engine’s repositories and runtime mechanisms, with service-specific startup sequences. The application manager and module registry extend the common startup; the schedule manager uses its own two-phase sequence. A common engine does not mean their readiness dependencies or initialization order can be ignored.

Match the caller to the door

Sharing a service does not mean sharing one credential or one permission model for all its endpoints.

DoorCallerPurpose
Scheduler administrationApplication-manager service identityInspect or manage scheduler state
Application job synchronizationAuthenticated applicationSynchronize that application’s jobs
Runtime’s own scheduleAuthorized runtime principalRead its own schedule and history

This selected middleware from crontabs-scheduler.crontab.controller.ts protects the administrative scheduler routes. Comments and later route registration are omitted; the excerpt stops inside setupRoutes.

protected setupRoutes(): void {
  this.router.use((req, res, next) => {
    const middleware = this.authMiddleware.create({
      allowPlatformServices: true,
      allowApplications: false,
      expectedAudience:
        PlatformApplicationType.PLATFORM_CRONTABS_BATCHES_MANAGER,
    });
    return middleware(req, res, next);
  });

  this.router.use((req, res, next) =>
    this.authzMiddleware.requireAppsManager()(req, res, next),
  );

The first layer authenticates an appropriate service token and audience. The second requires the application manager’s identity. The application synchronization and runtime self-read endpoints deliberately use other contracts; do not apply this administrative example to all scheduler traffic.

Separate operational control from customer authority

A platform environment hosts registered applications. Each application can then have its own application-wide administrators and customer organizations. Those application roles govern its resource operations; they are not the credentials the application manager uses to operate the shared platform.

Provision the shared services and their access before bootstrapping applications. Inspect each readiness endpoint and the specific authenticated exchange needed by the caller. A generated service definition establishes the intended deployment, not a running multi-region service or a guarantee of uninterrupted operation.

Prepare another application for its first start Mechanism

Register an application with its own identity and service declarations. Wildo prepares its platform records, service access and first application records, then derives the configuration its runtimes need.

The local workflow can add an application to a running platform without restarting its shared services. Deployment remains a separate step.

Example: Add a CRM beside an existing task application

The task application already uses the local platform. Registering the CRM prepares its own records and runtime configuration while keeping the shared platform running. The CRM’s users and customer organizations belong to the CRM, not to the task application.

Application registration prepares access before migrations and initial account seeding.
For engineers

From the application root, with its wildo.saas.config.ts and selected environment configured, use the registration-only local path when the platform is already running:

wildo local init --register-only

The active environment must be local, with the operator credentials established by wildo setup; this command rejects remote environments. The platform must already be reachable and healthy. This path skips shared cleanup, platform initialization and substrate convergence. Full local initialization has a different effect and refuses when an active development session owns the platform. The registration-only path lets another application join without taking ownership of that shared lifecycle.

Author the application’s bootstrap policy and service requirements first. Operator credentials and generated key material are not application source literals. The first administrator’s application user types belong in the authored bootstrap settings; their actual login material comes through the initialization workflow.

Match the first administrator to a declared user type

For example, Wonder CRM declares this bootstrap selection in wildo.saas.config.ts (selected fields):

bootstrap: {
  firstAdmin: {
    userTypes: ['member'],
  },
},

The authoritative backend’s src/saas-config.backend.ts must declare the matching userTypes.member entry and its authentication policy. Registration rejects an empty selection, a missing authoritative backend or configuration file, and user types absent from that backend. A user type identifies a category of user; it is not an administrator password or role declaration.

The initialization workflow can also receive a distinct approving administrator. That identity is optional for bootstrap, but application-wide access grants require another administrator to approve the request. Creating one administrator does not by itself make that two-person workflow operable.

Prepare the administrator who can approve wider reads

Some application-wide directory and audit reads need a temporary application-wide access grant. That request always starts pending and must be approved by a different application super-administrator. This is not a requirement for every application-scoped operation; the declared read admission determines when it applies. See product and customer authority for the two grant scopes.

The approving identity belongs in the selected environment’s authored secrets.json, in the approver block with email, firstName, lastName and password. Keep it outside application source and version control.

Starting pointHow the approving identity reaches registration
Fresh local application created by wildo initWith no existing secrets file, the CLI accepts all four WILDO_INIT_APPROVER_* inputs or derives a separate local identity from the first administrator
Existing environment or setup-created secretsPreserve the current secrets and author the complete approver block. Setup reconfiguration can fill an absent identity using its local or non-local policy; completed setup and generic additive secret provisioning leave it unchanged
Non-local environmentSupply the named approving administrator and their own password in the environment’s secrets; local identity derivation is not a remote provisioning policy

The four explicit inputs are WILDO_INIT_APPROVER_EMAIL, WILDO_INIT_APPROVER_FIRSTNAME, WILDO_INIT_APPROVER_LASTNAME and WILDO_INIT_APPROVER_PASSWORD. The local fallback replaces an email’s sub-address with +approver and shares the development password. It creates a different identity so a developer can exercise both decisions; it does not prove that two people participated.

For an existing local application on a running platform, add the block and run wildo local init --register-only. Registration forwards the identity to the application-manager seeding phase. Repeated seeding preserves an existing approver’s password instead of resetting it. Check the seeding outcome and sign in as the intended approving account before relying on the workflow. Remote environments use their provisioning workflow, not this local command. Older secrets without an approver remain readable; successful registration alone does not establish that application-wide approval is usable.

Derive every runtime from the same registration

The following framework implementation excerpt comes from application-runtime-bootstrap.service.ts; explanatory comments are omitted. Application authors do not call these helpers individually. They show why the backend, companion, workers and minions receive projections of the same prepared target.

const prepared = await AppBootstrapService.prepareApplicationEnvTargets(registeredTargetInput);
if (!prepared) {
  throw new Error(
    'Application bootstrap resolved no application target. Check wildo.saas.config.ts and the selected application slug.',
  );
}
const projectionInput = { ...registeredTargetInput, prepared };

const applicationBackends = await AppBootstrapService.buildApplicationBackendTargets(
  projectionInput,
);
const companionApis = await AppBootstrapService.buildApplicationCompanionTargets(
  projectionInput,
);
const applicationWorkers = await AppBootstrapService.buildApplicationWorkerTargets(
  projectionInput,
);
const applicationMinions = await AppBootstrapService.buildApplicationMinionTargets(
  projectionInput,
);

The workflow synchronizes the projections, then performs a signed platform-to-platform configuration read as the application manager, naming the target application slug. It checks that the returned slug matches and that the frontend-services projection is nonempty. This probe does not authenticate using the application’s backend primary secret.

A declaration-reapply dry run then compares the stored configuration with wildo.saas.config.ts: an empty updatedFields result establishes declaration coherence; any proposed change fails verification. Preparing once avoids mixing separately obtained registration or credential snapshots between runtime files.

These checks establish the selected application’s managed configuration, not its first login or every runtime’s health. Check the running application through its health and readiness endpoints, then verify its required services and first login independently.

Keep provisioning, migrations and seeding distinct
StageWhat it establishesOwner
RegistrationPlatform-side application, owner and configuration recordsApplication manager
Service preparationRequested database/broker/storage access and key materialConfigured provisioning services
Application migrationsTables and schema required before relational writesCLI bootstrap’s migration step
First recordsInitial application organization, administrator and credentialsApplication-manager seeding phase
Workload deploymentProcesses running in the target environmentDeployment workflow

The lifecycle service supports provisioning before seeding. The CLI can apply application migrations and register again with seeding required. Its appDatabaseSeeding report distinguishes DEFERRED, SEEDED and NO_PERSISTENCE_TARGET; it is not a per-service readiness report.

Registration uses upserts so a repeated attempt can continue existing setup. It is not an all-or-nothing infrastructure transaction. Some provisioning branches log and continue when their administrative service is unavailable; inspect the actual selected services, seeding outcome and runtime bootstrap verification. Explicit override paths can replace material, so routine registration and destructive reinitialization remain different operations.

Each call belongs to one explicitly identified platform environment. The application manager does not publish application workloads, and successful registration alone does not establish that an application is serving traffic.

Give each application its own startup configuration Mechanism

Each application resolves configuration for its own identity. Wildo combines environment connections with platform-managed metadata and credentials, then applies the application’s authored behavior.

Operators can update managed settings without rebuilding the application image. Running processes pick them up on their next start.

Example: Change an application's managed settings

An operator updates a registered application’s managed token policy. Its next startup receives that policy through the application manager. Another application’s configuration remains separately identified, and processes already running do not receive an automatic live update.

Application startup requests configuration and secrets from the application manager, then combines those responses with its environment settings.
For engineers
SourceHoldsConsumed when
Runtime environmentApplication/service identity, platform URL and backing-service connectionsProcess startup
Platform metadataApplication identity, declared runtimes/providers/capabilities, public keys and token policyBootstrap fetch
Platform secret responseCredential material allowed for the calling identitySeparate authenticated fetch
Application sourceApplication-specific behavior and configurationConfiguration resolution

Generate runtime files from the authored application and environment inputs. Do not maintain a second hand-written copy of platform metadata in each process. Bootstrap still requires environment material: the application has to know who it is, where the manager is and how to authenticate before it can ask for anything.

Author behavior in its owning file
Authoring surfaceWhat to declareWhat stays elsewhere
wildo.saas.config.tsApplication identity, services, modules, capabilities and providersRuntime credentials and resolved database endpoints
backend-api/src/saas-config.backend.tsUser-type authentication policy, passkey relying party, email sender, frontend login exposure and storage ceilingsPlatform-managed JWT, runtime and database branches
Selected infrastructure environmentOperating environment and backing-service connectionsApplication-specific authentication policy

For example, these selected fields from Wonder CRM’s backend-authored configuration describe its local passkey identity. The surrounding authentication settings remain required:

auth: {
  passkeyConfig: {
    rpName: 'Wonder CRM',
    rpId: 'localhost',
    maxPasskeysPerUser: 10,
  },
},

Use the deployed application’s real relying-party domain outside local development. The final configuration requires the user-type auth posture, passkey configuration and email.from; unresolved placeholders are rejected at startup. For a login request naming both a frontend service and user type, a missing or disabled frontendServices.<service>.usersManagement.<userType>.auth entry causes refusal. That check is scoped to requests carrying both values, not a claim that every authentication route behaves identically.

The backend configuration merger rejects changes to protected platform/runtime fields and validates the resolved configuration. These are enforced ownership boundaries, not simply file-organizing conventions. See passkey configuration for the authentication contract.

Authenticate the bootstrap before assembling configuration

This selected implementation from app-configuration-loader.backend.service.ts shows the application’s identity on the request and the separate data sources. Comments are omitted; these are loader internals, not application setup code to copy into a backend.

this.applicationEnvConfig = this.loadPlatformEnvironmentConfig();
this.serviceName = this.applicationEnvConfig.SERVICE_NAME;

this.axiosInstance = axios.create({
  baseURL: this.applicationEnvConfig.PLATFORM_APPS_MANAGER_URL,
  timeout: APPS_MANAGER_REQUEST_TIMEOUT_MS,
  headers: {
    [WildoHeaderKeys.CONTENT_TYPE]: 'application/json',
    [WildoHeaderKeys.SERVICE_ID]: this.serviceName,
    [WildoHeaderKeys.PLATFORM_SECRET]:
      this.applicationEnvConfig.PLATFORM_APPLICATION_PRIMARY_SECRET,
    [WildoHeaderKeys.APPLICATION_ID]:
      this.applicationEnvConfig.APPLICATION_ID,
  },
});

const appConfig = this.buildConfigFromEnv();

const appSecrets = await this.loadPlatformManagedSecrets();

const { appSlug } =
  await this.fetchAndMergeBootstrapConfigMetadata(appConfig);

Identity headers select and authenticate the caller; they are not a requested application switch. The bootstrap secret is credential material, never a value to paste into public examples. The timeout bounds each request.

The loader fetches reduced bootstrap metadata, not the entire application configuration. It validates responses against the managed contracts before merging their fields. Backing-service connection settings remain environment-owned. Application behavior is resolved from its authored source, while operator-owned service-auth token lifetimes retain their authority at signing.

Keep application-managed and platform-managed material separate

The stored configuration uses appManaged and platformManaged branches. Materialization combines the appropriate branches for the consumer rather than persisting a complete process-specific runtime object. The application-facing channel rejects attempts to mutate protected platform-managed secret branches.

Configuration and secrets travel through separate authenticated responses. A process cannot bootstrap simply because an HTTP response returned 200: required identity and key metadata must also parse correctly. Development retries are bounded; they accommodate startup ordering but do not establish a usable configuration when the final request still fails.

Derive different access from the same registration
RuntimeBacking-service access derived from its declaration
Application backendThe application’s provisioned set
MinionIts declared access; absent resourceAccess excludes data capabilities
WorkerNo backing-service capabilities through this runtime-access resolver

A present empty resourceAccess object differs from an absent declaration: it enters the minion data-capability branch. That topology allowance is not an unrestricted resource grant. Resource operations and credential minting still apply their own declaration checks.

Secret delivery removes shared principal identity lists. For non-backend callers it strips provider-derived credential paths before overlaying only that principal’s minted section, including when the section is absent. The backend exception is based on principal kind. One prepared registration therefore does not mean every process receives the same credentials. See per-runtime credentials.

Choose the bootstrap exchange the runtime needs
ExchangeCaller and purposeResult
Worker bootstrapAuthenticated request for a worker declared on the applicationWorker metadata and public verification keys, including rotation material; not a signing private key
Storage sessionVerified application/runtime caller within its allowed application or subresource scopeShort-lived, prefix-confined storage credentials; root storage credentials remain with the manager
Platform attestationA minion principal with recognized stored platformAccess scopes, naming an allowed platform audienceA signed identity and scope statement limited to that audience; workers cannot request minion authority

These exchanges are separate from ordinary configuration and secret loading. A worker does not need every exchange merely because they share a manager. Follow scoped storage sessions and per-runtime credentials for the detailed boundaries.

Publish token policy before restarting

For an existing local application, author both token lifetimes in its selected wildo.infra.local.config.ts. The values below are an example policy, not a recommendation for every application:

// Within the selected infrastructure configuration:
serviceAuth: {
  accessTokenExpirationMinutes: 22,
  refreshTokenExpirationDays: 9,
},

From that application’s workspace, refresh the selected environment snapshot, then register it:

wildo config sync --env local --domain config
wildo local init --register-only

The config sync step reloads the authored infrastructure file into config.json; registration-only reads that snapshot. Config sync also refreshes related deployment artifacts and can prepare or register application targets. Registration sends serviceAuth to the application manager. After finding or creating the application’s managed record, initialization reconciles those two JWT lifetimes even when no other configuration changed. This policy reconciliation preserves the other stored settings and does not require broad initialization override. Registration can also reconcile other supplied declarations; it is not a token-policy-only command.

StepWhat to verify
AuthorThe selected infrastructure environment contains the intended serviceAuth values
RegisterInitialization completes for the intended application; its stored platformManaged.jwt contains both values
Start the affected runtimeBootstrap retrieves the policy into jwt.accessTokenExpirationMinutes and jwt.refreshTokenExpirationDays

Omitting the entire serviceAuth block preserves an existing policy during ordinary registration. A present block applies schema defaults to omitted members, so author both values when you intend a complete explicit policy. First registration uses defaults when no policy is supplied. Broad override retains its wider reset behavior and is unnecessary for this change.

Already-running processes keep their current snapshot. Restart or redeploy the affected runtime through its normal lifecycle after the registration succeeds. This does not migrate data, restart services automatically, or retroactively change the expiration claim of tokens already issued.

Share the clock, keep each application's work separate Mechanism

Applications declare recurring work without giving every running copy its own independent timer. A shared platform scheduler holds the schedules and dispatches each tick to the intended application’s queue.

The receiving runtime verifies the signed instruction and performs the work. Scheduling, delivery and business completion remain separate stages that an operator can inspect.

Example: Schedule two products without mixing their jobs

A customer portal creates a daily export while an operations tool refreshes external records. Both use the platform’s clock, but each dispatch names its own application and reaches its own configured queue and runtime.

A shared scheduler sends signed ticks to separate application queues.
For engineers

Wonder Todos declares a cron-mode minion in minions/marketing-scrapper/wildo.minion.config.ts. This selected configuration omits display metadata and comments:

import { defineMinionConfig } from '@wildo-ai/platform-config-lib';

export default defineMinionConfig({
  version: 1,
  name: 'marketing-scrapper',
  runtime: { type: 'docker', language: 'typescript' },
  mode: 'cron',
  schedule: '0 */6 * * *',
  reinstantiation: {
    policy: 'kill_previous',
  },
  resources: { cpu: '500m', memory: '512Mi' },
});

The minion must also be declared in wildo.saas.config.ts, with its path and resource/platform access. Its work implementation belongs to that runtime. Synchronization brings the declaration into managed configuration; startup schedule synchronization registers the application’s jobs with the scheduler.

mode and schedule select recurring delivery. reinstantiation.policy controls replacement during deployment: the Kubernetes generator maps kill_previous to Recreate and let_run to RollingUpdate. It does not make successive cron handlers mutually exclusive. If overlapping ticks could duplicate an export or an external API charge, the application handler must coordinate that work and make its effects idempotent.

Follow the application identity through dispatch

The authenticated jobs-sync endpoint derives applicationId and the advertised broker virtual host from the application’s identity, not a target application supplied in the request body. A full sync creates or updates jobs and removes jobs no longer declared by that application.

The automatic resource-cron, custom-batch and minion extractors register their expressions in UTC, including the minion shown above. These declarations do not supply a timezone field. The lower-level application jobs-sync API accepts an explicit timezone, which the scheduler preserves; its stored-job default is UTC. Do not read that lower-level support as local-time or daylight-saving behavior for the illustrated declaration.

The scheduler validates expressions and refreshes its active job set. A dispatch names exactly one operation, custom batch or minion. The publisher selects its queue accordingly:

const queueName = input.minionName
  ? QueueNamingUtils.buildMinionTickQueueName(input.applicationId, input.minionName)
  : QueueNamingUtils.buildQueueName(input.applicationId, 'scheduled');

That selected implementation keeps a minion tick out of the backend’s ordinary scheduled-work queue. Both are still inside the application’s configured virtual host. The token signer names the target and work; the receiver verifies the scheduler’s signature before interpreting the instruction.

Bring up the queue owner before publishing

The receiving runtime declares its queue: the application backend owns the scheduled-work queue, and each minion owns its named tick queue. The scheduler checks that the destination already exists and refuses publication if it is absent; it does not create a replacement queue with guessed settings.

Repair the receiving runtime, queue topology or broker permissions before requesting fresh work. Queue existence alone does not prove a consumer is healthy, and a message retained by the broker can outlive its signed authorization.

Distinguish the stages when operating it
StageWhat its evidence establishes
Schedule synchronizationThe platform accepted the application’s declared jobs
Tick publicationThe scheduler submitted the signed dispatch to the broker
Runtime executionThe appropriate backend or minion received and handled the work
Business resultThe intended record, export or other effect actually exists

A published tick is not a completed business job. Inspect the receiving runtime and resulting effect when diagnosing a missed export. A broker-refused virtual host enters bounded retry quarantine; it needs provisioning or permission repair, not repeated immediate attempts.

Know which failures retry
Failure stageInherited behaviorWhat to inspect or supply
Configured schedule synchronization failsApplication startup continues; synchronization retries after 1 second with exponential backoff capped at 5 minutes, until success or shutdownCheck sync logs and manager connectivity. A running application does not prove that its schedules were accepted
Sync service is uninitialized or its manager is unconfiguredSynchronization is skipped without scheduling that retryCorrect initialization or manager configuration; waiting alone does not register jobs
Broker refuses a virtual hostConnection attempts for that host are quarantined with increasing delays, from 1 minute up to 30 minutesRepair the host or publishing permission. This backoff is separate from application-job retry
A minion handler throws, or its tick fails verificationThe consumer negatively acknowledges that delivery without requeueing itInspect the minion failure and resulting business state; the next scheduled tick is fresh work, not automatic replay of the failed transaction

Successful minion handling acknowledges the tick. The no-requeue rule above describes the consumer’s explicit failure handling; it does not promise that broker or connection failures can never cause redelivery. The application owns recovery of an incomplete business effect, including deciding whether and how it is safe to replay it.

Scheduler replicas coordinate through a shared lease: the leader arms schedules and rechecks ownership before each scheduled tick; standbys disarm their tasks. A lease check is not end-to-end deduplication: a stalled publisher can race with takeover after passing the check. Application-declared schedules are reconciled through startup sync; administrative scheduler operations are a separate control surface. Keep handler retry/idempotency policy and recovery of interrupted work explicit.

When a runtime misses a tick

A signed tick has a five-minute token lifetime. The verifier allows 30 seconds of clock tolerance; a queued message is not an indefinitely reusable authorization to run the work.

What happensWhat the operator should expect
A runtime receives a tick after its accepted lifetimeVerification refuses the expired instruction before executing its work
Scheduled backend execution or verification failsThe consumer rejects without requeue; its scheduled dead-letter copy supports inspection
Minion tick processing failsThe consumer also rejects without requeue; it does not automatically retry that tick
A later scheduled occurrence arrivesThe scheduler creates a fresh signed tick, not a replay of the missed business operation

After an outage, inspect the receiving runtime and the business result before deciding what to recover. Reusing an expired token does not repair missed work. A catch-up action must deliberately select the missing work and avoid duplicating completed effects; the next cron occurrence alone does not establish that recovery happened.

Define administrative reach

Separate product administration from customer administration Guarantee

Each application has its own product-wide roles and customer-organization roles. Managing one customer account does not make someone an administrator of the whole product.

That distinction lets a product support its customers while keeping customer membership, administrative responsibility and temporary support access separate.

Example: Let a customer manage its team

A customer owner invites colleagues and manages their account. A product administrator operates the wider application. The owner’s organization role does not become a product-wide role, and the product administrator still needs the appropriate declared access to cross into customer records.

Product administration and customer-account administration have distinct scopes.
For engineers

ResourcePrimaryScope.APPLICATION means the scope of one hosted application. It is distinct from ResourcePrimaryScope.ORGANIZATIONS, which describes its customer organizations. It does not mean the Wildo application-manager service or authority over other hosted applications.

These selected entries from roles.shared.defaults.ts show the separate default role tables. They are fragments of two declarations, not one complete configuration object.

[CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN]: {
  role: CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN,
  inheritFrom: CORE_APP_ROLES.APP_ADMIN_BILLING_MANAGER,
  isSystemRole: true,
  relatedPrimaryScope: ResourcePrimaryScope.APPLICATION
}

// Separately, in DEFAULT_ORGANIZATION_ROLES:
[CORE_ORG_ROLES.ORG_OWNER]: {
  role: CORE_ORG_ROLES.ORG_OWNER,
  inheritFrom: CORE_ORG_ROLES.ORG_ADMIN,
  isSystemRole: true,
  relatedPrimaryScope: ResourcePrimaryScope.ORGANIZATIONS
}

A role is resolved in the table for its scope, including that table’s inheritance chain. Writing the name of an application role into an organization membership does not create application authority. Organization-unit roles are narrower still; a unit assignment is not an organization-wide membership.

Declare the operation and the reach it needs
Intended actionAuthority to consider
Manage one customer’s membersOrganization membership, operation roles and grant ceiling
Operate application-wide resourcesApplication-role table and the resource’s operation contract
Support a particular customer from outside its membershipDeclared cross-tenant variant plus usable tenant access grant
Enumerate permitted application-wide directory dataDeclared read surface plus usable application-wide grant
Read application audit dataAdministrator permissions; a usable application-wide grant is also required when auditTrail.applicationReadAccess is independent-approval

Application authority is necessary for product administration, but it is not blanket permission to read or edit every customer’s records. The authorization layer separately evaluates the operation’s admission and the applicable grant. Customer-specific and application-wide grants have different scopes; an empty customer identifier is not a substitute for the latter.

Match the approval to the grant’s scope
GrantApproval and lifecycle
Customer-specific accessBound to one organization. With a usable owner and approval required, it starts pending; tenant authority approves or denies it. When approval is disabled or no usable owner exists, the request can become active immediately.
Application-wide read accessBound to one application, not one customer. Requests always start pending; approval requires a different application super-administrator. There is no tenant-policy or ownerless auto-approval branch.

An active grant must still be usable for the requested access, including its expiry and revocation state. Neither grant replaces the operation’s role and admission checks. The registration workflow explains how to provision the second identity, including the deliberate single-developer local convenience.

Keep recovery authority explicit

The framework’s administrative floors protect against writes that would remove the last usable administrator. A startup report also detects states introduced outside those write paths, such as a restore or direct database edit.

Within the application’s role hierarchy, an organization may have a higher application-level recovery path; the application super-administrator floor has no still-higher role in that hierarchy. Recovery from an externally introduced zero-administrator state is an operator procedure. Sharing Wildo’s infrastructure does not silently create a cross-application customer-support identity.

Open specific support actions, not blanket access Mechanism

Support access is attached to a particular action. Wildo checks whether that operation permits an application operator to cross an organization boundary and whether the operator has a usable grant for the target organization.

Restoring an account or repairing ownership can have a deliberate path without opening general editing or immediate permanent deletion across customer accounts.

Example: Restore an organization's ability to administer itself

An operator obtains access for a stranded organization, then uses the ownership-repair action to promote an active member whose linked user is also active. The action does not create a new member or allow arbitrary changes to the organization’s records.

When no usable owner remains, the grant service checks that condition and can automatically approve recovery access; the operator cannot simply assert it. The grant approval policy explains when customer approval is required.

The illustration shows another specific support action: restoring an organization awaiting deletion during its retention window. Ownership repair follows the same principle, with its own operation and inputs.

A declared restoration action uses an active grant for its target organization; irreversible purge remains closed.
For engineers

The ownership-repair operation on organizationMembers is a concrete example. This selected fragment removes comments and the neighboring notification declaration; it belongs inside the resource’s operation configuration:

[OrganizationMembers_Operations.GRANT_OWNERSHIP]: {
  variants: [
    {
      variantType: ResourceOperationVariantType.API_CALL,
      isDefault: true,
      roles: [CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN],
      riskLevel: ResourceOperationRiskLevel.CRITICAL,
      resourceOperationLike: CoreResourceOperation.UPDATE,
      admitsCrossTenantPlatformAdministration: true,
      requestDto: z.object({
        justification: z.string().min(1).max(1000),
      }),
    },
  ],
},

The role gate and admitsCrossTenantPlatformAdministration do different work: one checks authority, the other opts this variant into crossing. The implementation supplies the narrow ownership change; the declaration alone does not implement a business action. Its required justification belongs to the request, while grant approval follows the separate access-grant lifecycle.

Establish access before invoking the action

Request a temporary platform access grant for the target organization and follow its approval policy. The authorizer looks for a usable grant for this operator and organization within its time window. A lookup failure refuses access. The request-access operation has a narrow bootstrap exception because requiring an existing grant to request one would make the flow unreachable.

Membership is established from authenticated role entries, not an organization ID supplied in the URL. Unit-scoped authority is not treated as organization-wide membership. Admission is checked in the role/collection path and the fetched-record path so that opening one route does not silently open another.

Repair ownership without replacing existing permissions

The target must be an active membership linked to an active user. An invited or suspended member, or a disabled user, cannot restore usable administration; the action refuses that target instead of reporting a cosmetic repair.

The implementation makes roles server-authoritative and includes the linked-user read in the membership transaction. A caller cannot inject its own role set through a lower-level service call. The following selected implementation preserves existing roles and makes an already-owner retry succeed:

const currentRoles = normalizeRoleArray(currentObject!.roles);

if (rolesConferOrganizationOwner(currentRoles, organizationOwnerConferralResolver())) {
  return { roles: currentRoles };
}

// Add ownership without removing the member's other roles.
return { roles: [...currentRoles, CORE_ORG_ROLES.ORG_OWNER] };
Target or inputResult
Active member and active linked userAdds ownership while preserving existing roles
Member already has an owner-conferring roleKeeps the current roles; retry is idempotent
Invited or suspended membership, or unusable linked userRefuses the repair; resolve that lifecycle state first
Caller supplies a different role setThe implementation’s authoritative role result takes precedence
Keep the operation’s purpose narrow
Existing actionWhat makes the crossing deliberate
Activate or restore an organizationA named lifecycle transition with a recovery purpose
Request deletion with a windowA declared transition that retains a restore path
Grant ownershipPromotes an eligible active member; does not become a general membership editor
Immediate purgeNo cross-tenant admission in the default declaration

The crossing flag is an authoring decision, not a client header or runtime switch. Review it with the resource specification and the implementation’s actual writes. Reversibility is a design choice in these default operations, not a type-system rule preventing an application author from declaring an unsafe custom action.

At a fetched-record tenant denial, the response conceals existence with not-found semantics. Other earlier authorization failures can be refused before a record is fetched. Access evidence records the applicable observation; it does not establish that the action later completed successfully.

Understand access and authority changes

Make access across customer boundaries visible Guarantee

A support action outside the operator’s organization memberships deserves a recognizable record. Wildo’s authorization layer produces a dedicated observation with the operator, target organization, operation and the grant resolved for that crossing.

The crossing record belongs to the reached customer organization. People authorized to read that organization’s audit history can inspect it through the audit-log access surface. It is not only an operator-side trace.

This helps distinguish approved support work from unexpected reach. It complements the access decision; it does not replace the permission checks or prove that the business action completed.

Example: Explain why support restored an account

An investigator connects an organization-restoration attempt to its operator and temporary access grant. The grant lifecycle provides the justification and approval history, while the business operation’s evidence shows what changed.

An audit record identifies the actor, customer account, action and access grant.
For engineers

This selected payload comes from recordCrossTenantAdministrativeAccess in AuthorizationsBackendService. It is framework emission code, not an event an application should re-emit from every handler:

auditLogsService.logCrossTenantAdministrativeAccess({
  operatorUserId: executionContext.initiatorIds?.userId,
  targetOrganizationId: rowOrganizationId,
  resourceType: resourceIdentifier,
  operationIdentifier: String(operation.operationIdentifier ?? ''),
  variantType: operation.variantType as ResourceOperationVariantType,
  variantKey: operation.variantKey,
  targetResourceIds: resourceObject?._id ? [String(resourceObject._id)] : undefined,
  authorizingGrantId: (executionContext as { authorizingPlatformAccessGrantId?: string }).authorizingPlatformAccessGrantId,
  authorizingGrantScope: (executionContext as { authorizingPlatformAccessGrantId?: string }).authorizingPlatformAccessGrantId
    ? PlatformAccessGrantScope.TENANT
    : undefined,
  admissionDeclared: operation.admitsCrossTenantPlatformAdministration === true,
  initiatorRoles: initiatorRoles.flatMap(roleInfo => roleInfo.roles ?? []),
  executionType: executionContext.executionType,
})

authorizingGrantId connects this use of access to the grant’s own lifecycle. For a row crossing, authorizingGrantScope identifies the tenant-grant collection; both are absent when no tenant grant was resolved. An application-wide read grant does not substitute for row admission. admissionDeclared states whether the operation opted into crossing. Neither field alone is a business-success result: a declared operation can still lack a usable grant or fail later.

Read the sequence correctly

The fetched row identifies the organization. Organization-wide role entries determine whether the caller belongs to it. For a crossing, admission resolves the usable grant, the observation is emitted, and the tenant-boundary decision then permits or refuses the request.

This ordering includes permitted crossings and refusals reaching that observation point. It is not a record of every rejected HTTP request: a request rejected before a target row is known follows its own authentication or authorization evidence path.

EvidenceQuestion it answers
Grant lifecycleWho requested, approved, revoked or timed out the access?
Cross-tenant observationWhich operation reached across the boundary, under which resolved grant?
Operation/change evidenceWhat did the business action actually change?
Application-wide read evidenceWhich broader read occurred without a single target organization?
Example: investigate a support crossing

Use the customer’s audit inspection API with an authenticated session authorized for that organization. This illustrative query selects crossing observations; the same route’s access rules still apply:

GET /organizations/{organizationId}/audit-logs?eventType=cross_tenant_administrative_access

Inspect the returned data records for the operator, eventData.targetOrganizationId, operation and target resource IDs. Resolve eventData.authorizingGrantId in the collection named by eventData.authorizingGrantScope to find the justification and approval history. A missing grant is not enough to determine the access decision: application audit reads use administrator permissions by default, while the optional independent-approval policy requires a grant. Other grant-controlled operations still require their grants. Then inspect the business change separately: observing a crossing does not prove that restoration or another action succeeded.

Preserve the scope when a read reaches several customers

Declared cross-tenant reads outside organization-scoped resources use a second observer for READ, LIST and SEARCH. Enrollment follows the executed operation variant. The observer groups the foreign organizations actually returned, with their target record IDs; a single-record read is included, while an empty result or records belonging only to the caller’s organizations produces no crossing.

Organization-scoped resources already observed during authorization are excluded from this second path to avoid double counting. Internally initiated requests and webhook callbacks are also excluded. COUNT returns no attributable records and is not enrolled.

Each resulting crossing is stored under its target organizationId, so it participates in that customer’s authorized audit access. This attribution does not grant audit-reading permission or guarantee delivery.

For application audit reads, auditTrail.applicationReadAccess defaults to role-based; independent-approval adds a time-bound grant approved by another administrator. Both retain the read observations. This policy does not change other application-wide or tenant-support access.

When a grant is recorded, interpret authorizingGrantId together with authorizingGrantScope. Tenant and application-wide grants live in different collections, so the ID alone does not identify where to resolve the authorization. If both grants are present, the observation names the narrower tenant grant.

Recorded scopeGrant to resolve
TENANTThe temporary grant for the customer organization
APPLICATION_WIDEThe grant authorizing the broader application read

An application-wide directory read has its own observation, including the returned count and application-wide grant. It does not invent a target customer for a read spanning the directory.

Monitor evidence delivery as well as access

emitAuditObservation keeps audit delivery from changing the request’s authorization result. If the service is unavailable or emission fails, the observation seam reports the loss through diagnostics and metrics rather than silently converting the business request into an audit-store failure.

Delivery remains best effort. Do not interpret an absent event as proof that no access occurred, or a crossing event as proof of a successful modification. The application must retain and route evidence according to its operating policy and investigate delivery-loss signals.

Application-wide reads use their own scope classification and access-grant policy. A directory listing with no single target organization cannot be represented faithfully as a row-specific customer crossing.

Explain who changed customer permissions Guarantee

Changing a person’s roles changes what they can do. Wildo records that change as an authority event, identifying the actor and the previous, requested and resulting roles where applicable.

Organization-wide memberships and assignments inside one unit remain distinct. The evidence can explain both broad customer administration and a narrower team-level grant.

Example: Understand how a member became a manager

A customer administrator changes a member’s role. The evidence keeps the before-and-after roles and the requested set, while a separate unit assignment shows that another permission applies only inside one team.

An authority-change record explains who changed a role and whose authority changed.
For engineers

Declare and invoke roles through the resource’s membership operations and their registered custom implementations. Their role-ceiling and administrative-continuity checks still apply. Direct database edits bypass this operation lifecycle and cannot be expected to produce its semantic evidence.

The following selected emission from organization-member-custom-implementation.backend.service.ts shows why authority evidence contains more than the final row:

await emitAuthorityChangeAuditWhenDurable({
  serviceOptions: utils.serviceOptions,
  effectKey: ['organization-member-roles-changed', memberId, pending.operation].join(':'),
  logger: utils.logger,
  failureContext: { operation: pending.operation, memberId, organizationId },
  emit: async () => {
    const auditLogsService = container.get<AuditLogsBackendService>(SAAS_SERVICE_TYPES.AuditLogsService);
    await auditLogsService.logOrganizationMemberRolesChanged({
      organizationId,
      memberId,
      userId,
      previousRoles: pending.previousRoles,
      newRoles: pending.newRoles,
      requestedRoles: pending.requestedRoles,
      operation: pending.operation,
      changedBy: resolveAuditActorId(executionContext),
    }, executionContext);
  },
});

The operation stages its inputs before persistence, then emits through the durability helper. requestedRoles preserves what the caller authored; newRoles describes the resulting set. This matters when defaults, validation or grant ceilings distinguish the request from the stored authority.

Keep the scope visible in the event
ChangeEvent familyEvidence to inspect
Ordinary organization membership role changeORGANIZATION_MEMBER_ROLE_CHANGEDpreviousRoles, newRoles, requestedRoles, member and organization
Membership removal or invitation revocationORGANIZATION_MEMBER_REMOVEDRoles, previousStatus and revokedUnitAssignments, captured before deletion
Unit role assignmentORGANIZATION_UNIT_MEMBER_ROLE_CHANGEDpreviousRole, newRole, requestedRole, assignment and unit
Direct unit assignment removalORGANIZATION_UNIT_MEMBER_REMOVEDThe removed assignment and its revokedRole, captured before deletion
Named ownership repair (GRANT_OWNERSHIP)RESOURCE_OPERATION_PERFORMEDOperation identity, affected member, changedFields and role sets in fieldEvidence when changed
Application rolesSeparate application-scope authority eventsThe affected principal’s application authority, not customer membership
Include ownership repair in an authority review

The ownership-repair action uses update-like persistence, but its custom implementation is registered under its own operation identity. It does not inherit the ordinary UPDATE hook that emits ORGANIZATION_MEMBER_ROLE_CHANGED. Its critical risk instead triggers automatic RESOURCE_OPERATION_PERFORMED evidence; the roles field is marked for before-and-after evidence.

A query restricted to ORGANIZATION_MEMBER_ROLE_CHANGED can therefore miss ownership repair. Query the automatic RESOURCE_OPERATION_PERFORMED event family, then inspect the returned eventData.resourceType and eventData.operationIdentifier for the membership resource and GRANT_OWNERSHIP operation. Those nested fields are not LIST query filters. Inspect fieldEvidence for the role change; do not expect the specialized event’s requestedRoles field in this payload. An idempotent repair can leave the roles unchanged.

Correlate the operation evidence with the crossing observation by actor, operation, affected record and request context. Resolve the observation’s grant ID together with its grant scope to inspect the grant lifecycle. These are complementary records, not identical payloads or an atomic evidence bundle.

A unit assignment holds one role per assignment record. Treating it as an organization-wide role array would erase the scope that limits its authority. The audit subject identifies the acting principal; the affected user/member and assignment remain explicit event data.

Removing a membership also removes the record that explained its access. Capturing its status distinguishes a revoked invitation from an active membership removal; retaining the unit assignments explains the narrower authority removed with it.

Example: investigate a membership role change

After a role change, query the customer’s audit resource using an authenticated session authorized for that organization. This illustrative request uses the declared event-type filter; replace the organization placeholder and use the application’s API base URL:

GET /organizations/{organizationId}/audit-logs?eventType=organization_member_role_changed

The default LIST variant declares organization member, organization administrator and application super-administrator roles. The route and authorization context still constrain access: a role name or an organization ID in a URL does not grant access to another customer’s trail. See the audit inspection and export API for the full read/filter contract.

Read the returned data records as follows:

FieldHow to interpret the result
userId on the audit record, and eventData.changedByThe actor who made the change; the userId query filter selects this actor
eventData.userId and eventData.memberIdThe affected user and membership, not necessarily the actor
eventData.previousRolesAuthority before the mutation
eventData.requestedRolesThe submitted role set, when supplied
eventData.newRolesAuthority resulting from the operation

For example, if an administrator changes another member’s roles, the actor filter finds the administrator’s action while the event data identifies the member affected. Compare requested and resulting roles rather than assuming they are identical. A repeated unchanged role set emits no new role-change event; an empty result alone also cannot establish that audit delivery was healthy.

Record durable changes, not rolled-back intentions

The helper emits after the relevant write is durable and defers to an outer transaction’s post-commit lane when necessary. A rollback must not leave an event claiming a grant that never took effect. Repeating the same role set produces no role-change event.

Cascade removal follows the parent lifecycle’s evidence rather than fabricating an individual revocation for every child assignment. Audit routing also uses authenticated context; a system provisioning action can reach the shared audit store without an authenticated customer routing context.

These events explain changes made through the framework lifecycle. Preserve the supplied registrations and transaction context when extending that lifecycle, and monitor audit-delivery diagnostics rather than treating the event stream as an infallible transactional replica.

Give every environment its purpose. Keep one application to understand.

Local development, staging and production can share the application’s structure without sharing the same infrastructure or credentials.

Wildo carries declared choices into connections, process configuration and deployment inputs. You decide how each environment is operated and when it is ready for its users.

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.