Skip to main content
In an Ooneex project the main app/API and each microservice are independent processes. They do not share a runtime — they communicate over HTTP, each listening on its own port, and the API finds a microservice by resolving a URL it reads from configuration. The microservice:create generator wires this up for you: it assigns the new service a free port, declares it in modules/app/app.yml, and adds an entry to the project root .env.yml so you can supply the real URL per environment. The discovery is env-var-driven. The API never hard-codes where a microservice lives. Instead, app.yml names the environment variable that holds the URL, and the actual value comes from .env.yml for the current environment. Swap the value per environment (local, staging, production) without touching code.

How it works

ConcernWhere it livesValue
Main app / API portThe app process3000
Microservice portThe microservice’s own .env.yml (app.port)Next free port from 3001 upward
Service declarationmodules/app/app.yml under microservices:name + the env var name holding the URL
URL env var nameapp.yml url fieldPattern MICROSERVICE_<SNAKE_UPPER>_URL
Actual URL per environmentRoot .env.yml under microservices:You fill in url for each environment
Service-to-service transportOver HTTP at the configured URLhttp://... resolved at runtime
The main app/API always runs on port 3000. When you scaffold a microservice, the generator scans every modules/*/.env.yml for port: values already in use and picks the next free port starting at 3001 — so each microservice gets its own distinct port and no two services collide. That chosen port is written to the new microservice’s own .env.yml under app.port.

Service declaration in app.yml

So the API can reach a microservice, the generator declares it in modules/app/app.yml under a microservices: list. Each entry pairs a name with the name of the environment variable that holds the service’s URL:
modules/app/app.yml
microservices:
  # billing microservice
  - name: "billing"
    url: MICROSERVICE_BILLING_URL # env var name
The url value is not a URL — it is the name of the environment variable (pattern MICROSERVICE_<SNAKE_UPPER>_URL) that holds the actual URL. This indirection is what makes discovery environment-aware: the same declaration resolves to a different address in local, staging, and production.

Root .env.yml URL map

The generator also adds the microservice to the project root .env.yml under a microservices: map. This is where you fill in the real URL for each environment:
.env.yml
microservices:
  billing:
    url: ""
Set url to the address where the service is reachable in that environment — for example http://localhost:3001 locally, or the deployed hostname in staging and production. The declaration in app.yml reads this value through its named environment variable.

Per-microservice runtime config

Each microservice carries its own runtime configuration in its own .env.yml — its distinct port plus its own datastores. A service does not share the API’s Redis or Postgres; it gets its own:
modules/billing/.env.yml
app:
  port: 3001 # distinct, assigned automatically from 3001+

cache:
  url: redis://localhost:6379
pubsub:
  url: redis://localhost:6379
rate_limit:
  url: redis://localhost:6379

database:
  url: postgresql://ooneex:ooneex@localhost:5432/ooneex
The app.port here is the port the generator assigned. The Redis URLs (cache, pubsub, rate_limit) and the Postgres database.url are the service’s own backing stores — keep them separate so each service owns its data.

Talking between services

Communication is over HTTP at the configured URL. The API resolves a microservice’s address from its app.yml declaration (the named env var) and the value supplied in .env.yml, then issues an HTTP request to that address. Use the framework’s HTTP client utility (@ooneex/fetcher) to make the call — it is the standard way to perform HTTP requests in an Ooneex app. Because the address always comes from configuration, you never embed a hostname in code. Change the environment, change the URL, and the calling service follows.

Best practices

  • Supply URLs per environment. Fill in each microservices.<name>.url in .env.yml for local, staging, and production — inject production values through your platform’s secrets/env, not committed files.
  • Never hard-code URLs. Always go through the env-var-named declaration in app.yml so the address is resolved from configuration, not baked into source.
  • Keep datastores separate. Each microservice owns its Redis and Postgres (cache, pubsub, rate_limit, database); do not point two services at the same database for shared state — talk over HTTP instead.
  • Let the generator pick ports. It scans modules/*/.env.yml and assigns the next free port from 3001, so distinct ports stay collision-free; reserve 3000 for the app/API.
  • Talk over HTTP at the configured URL. Resolve the address from config and call the service with the HTTP client; treat each microservice as a remote process, not a local import.
  • Clean up on removal. Removing a service with microservice:remove strips its entry from modules/app/app.yml, the root .env.yml map, and its docker-compose block — let the generator do it so nothing dangles.
  • Microservice overview — what microservices are in an Ooneex project and when to reach for one.
  • Microservice structure — how a generated microservice is laid out.
  • microservice:create — the generator that assigns the port and writes these config entries.
  • Configuration — how .env.yml and app.yml are loaded and merged per environment.
  • Fetcher — the HTTP client utility for calling services at their configured URLs.
  • Deployment — supplying per-environment URLs and secrets when you ship.