# Multi-service applications

**Experimental.** The manifest format may still change. Service supervision and tasks run on macOS and Linux; on Windows only `project check` and `project plan` work.

An `[application]` table in `neutron.toml` describes services that keep running (an API, a web app) and tasks that finish (a build, a test run, a proof check). Each keeps its own native toolchain: Neutron runs the command you declare, as an argument list without a shell, and does not compile or wrap it. Projects without the table keep the single-language `neutron dev` behavior.

```toml
[application]
version = 1
name = "inventory"

[application.services.api]
path = "services/api"
command = ["go", "run", "./cmd/server"]
contract = "neutron/v1"          # a Neutron SDK service; port assigned at start

[application.services.web]
path = "apps/web"
command = ["pnpm", "dev"]
ports = [3000]
depends_on = ["api"]             # web receives NEUTRON_SERVICE_API_URL
ready = { http = "http://127.0.0.1:3000/", timeout = "30s" }

[application.tasks.test]
path = "services/api"
command = ["go", "test", "./..."]
timeout = "10m"
```

## Commands

| Command | What it does |
|---|---|
| `neutron project check` | Validates the manifest without running anything. |
| `neutron project plan [--json] [--service NAME]` | Shows services, tasks, ports and injected environment keys (never values). |
| `neutron dev [--service NAME]` | Starts the selected service and its dependencies in dependency order, in the foreground. |
| `neutron project run TASK... [--jobs N] [--json]` | Runs tasks and their dependencies; exits nonzero unless all succeed. |
| `neutron project spec --write` or `--check` `[--service NAME]` | Snapshots each Neutron SDK service's OpenAPI document, or fails if a snapshot is out of date. |

## Services

A service starts once each of its `depends_on` services is ready. Readiness is one of `http` (a URL that must return 2xx, no redirects), `tcp` (`host:port` accepting connections), or `path` (an HTTP path on the service's own single port), each with a `timeout`. A service that others depend on must be able to report readiness.

If any service fails to start, fails readiness, or exits, even with status 0, the whole session stops. There is no automatic restart; your tool's own watcher handles reloads.

**Stopping.** Interrupt once: dependents stop before their dependencies, each gets SIGTERM, then SIGKILL after its grace period. Interrupt again to skip the remaining grace. Services also stop if the terminal closes or the `neutron` process itself is killed; each service runs under a small supervisor process that stops its whole process tree when `neutron` goes away.

**Ports.** Declared ports are checked before anything starts, including listeners on `0.0.0.0`, `::` and `::1`, so a stale server cannot pass as the new one.

**Finding other services.** Each service receives `NEUTRON_SERVICE_<NAME>_URL` (for example `NEUTRON_SERVICE_API_URL=http://127.0.0.1:52811`) for every direct dependency with exactly one port. Explicit `env` entries always win.

## Neutron SDK services

`contract = "neutron/v1"` marks a service built on a Neutron SDK that follows the [framework contract](https://github.com/neutron-build/neutron/blob/main/FRAMEWORK_CONTRACT.md). For such a service the coordinator:

- assigns a free loopback port when `ports` is omitted, and passes `NEUTRON_HOST` and `NEUTRON_PORT`,
- treats `GET /health` as readiness unless `ready` says otherwise,
- allows the contract's 30-second drain before SIGKILL,
- reports, once ready, a degraded `/health`, a missing `/openapi.json`, or a spec that is not OpenAPI 3.1. These are warnings; the session keeps running.

The SDK must read `NEUTRON_HOST` and `NEUTRON_PORT` for an assigned port to work. The Go, TypeScript and Python SDKs do so from the current repository; declare `ports` explicitly with older releases.

## Typed clients between services

A Neutron SDK service describes its API at `/openapi.json`. `neutron project spec --write` starts the Neutron SDK services, waits until they are ready, and saves each document canonically as `openapi.json` in the service's directory. Commit that file: it is the reviewed interface, so an API change appears as a diff. `neutron project spec --check` fails when a running service no longer matches its snapshot, which makes it a useful CI step.

Clients are generated from the snapshot by an ordinary task using an existing generator, for example a TypeScript client from a Go API:

```toml
[application.tasks.web-api-types]
path = "apps/web"
command = ["./node_modules/.bin/openapi-typescript", "../../services/api/openapi.json", "--output", "src/api.d.ts"]
timeout = "2m"

[application.tasks.web-typecheck]
path = "apps/web"
command = ["./node_modules/.bin/tsc", "--noEmit"]
depends_on = ["web-api-types"]
timeout = "5m"
```

Renaming a response field in the API then fails `spec --check`, and after `spec --write` fails the typecheck wherever the old field was used.

## Tasks

A task succeeds when its command exits 0. `timeout` is required. `depends_on` may name only other tasks. `outputs` lists paths the task should produce; they appear in the result but are not cached or checked. Native tools keep their own caches.

`project run` is fail-fast: after a failure, timeout or interrupt, nothing new starts, running tasks are stopped, and the rest are reported as `skipped`. `--jobs` bounds concurrency (default: CPU count). With `--json`, a versioned result goes to stdout and task output to stderr:

```json
{
  "version": 1,
  "tasks": [
    { "name": "build", "status": "succeeded", "exit_code": 0, "duration_ms": 1840, "outputs": ["bin/api"] },
    { "name": "test", "status": "failed", "exit_code": 1, "duration_ms": 3120, "error": "exit status 1" }
  ]
}
```

Statuses are `succeeded`, `failed`, `timed_out`, `cancelled` and `skipped`. Verification and simulation tools such as `lake build`, `quint verify`, or a Modelica run are ordinary tasks. A passing task means the tool exited 0, not that a proof covers your implementation.

## Limits

- Services and tasks cannot depend on each other yet.
- An assigned port is free when chosen; another process could take it before the service binds it.
- A readiness probe proves something answered on the port, not which process answered.
- Child output is shown as written; secrets printed by a service are not redacted.

Runnable examples: [`cli/examples/application`](https://github.com/neutron-build/neutron/tree/main/cli/examples/application) (plain Go and Node services) and [`cli/examples/neutron-application`](https://github.com/neutron-build/neutron/tree/main/cli/examples/neutron-application) (a Go SDK API and a Neutron TypeScript app with a generated typed client).
