# Deploy locally with Aspire

**Goal:** run your whole NetScript workspace on one machine under .NET Aspire — scaffold the AppHost, bring up the resource graph (Postgres, Redis, every service and background processor), and watch it from the Aspire dashboard. This is the **local** orchestration companion to the [Deploy](https://rickylabs.github.io/netscript/orchestration-runtime/how-to/deploy/) recipe (which covers shipping to a remote target); for *why* the AppHost works the way it does, read [Orchestration with Aspire](https://rickylabs.github.io/netscript/explanation/aspire/).

> The order is: scaffold → orchestrate → database
>
> Aspire is
>
> step 2
>
> , before any database command.
>
> netscript init
>
> writes the
>
> aspire/
>
> AppHost;
>
> aspire start
>
> provisions Postgres and Redis and starts every process;
>
> only then
>
> do
>
> netscript db init/generate/seed
>
> work, because those commands migrate the database
>
> through
>
> the running AppHost. Run a db command with no Aspire up and it fails — there is no Postgres for it to reach. See
>
> [Database](https://rickylabs.github.io/netscript/netscript/data-persistence/database/) .

## Prerequisites

**What you need before you start**

| Name | Type | Description |
| --- | --- | --- |
| `A scaffolded workspace` | `netscript init` | Created WITHOUT --no-aspire, so the aspire/ AppHost folder exists. See the CLI reference for init flags. |
| `Docker daemon running` | `container engine` | Aspire provisions Postgres and Redis as local Docker containers. No daemon = the default workflow does not start. |
| `The Aspire CLI` | `aspire (external)` | The aspire restore / aspire start commands are the external .NET Aspire CLI, run from inside aspire/ — not netscript subcommands. |
| `A reachable port range` | `ports` | Dashboard :18888 (HTTPS) / :18889 (HTTP), OTLP :4318, services from :3000, plugin APIs :8091–8099, the Fresh app :8010. |

> No --no-aspire
>
> This recipe assumes the orchestration layer was scaffolded. If you ran
>
> netscript init my-app --no-aspire
>
> there is
>
> no
>
> aspire/
>
> folder and no
>
> aspire start
>
> — you start the Deno processes yourself and supply your own infrastructure. That path is covered in the
>
> Deploy
>
> recipe and the [Aspire explanation](https://rickylabs.github.io/netscript/explanation/aspire/).

## Step 1 — Confirm the AppHost was scaffolded

The AppHost is a small **TypeScript/Node** program (not C#) at `aspire/apphost.mts`, configured by `aspire/aspire.config.json`. `netscript init` generates it; you do not hand-write it. Verify it is on disk before going further:

```bash
# From the workspace root — these two files are the orchestration entry point.
ls aspire/apphost.mts aspire/aspire.config.json
```

`aspire.config.json` pins `language: "typescript/nodejs"`, `appHost.path: "apphost.mts"`, and the Aspire SDK version `13.4.6`. The graph inside the AppHost is **derived from your installed plugins** at boot via `composeAppHost` (from `@netscript/aspire/application`) — add a plugin and its API plus background processor appear in the graph; remove it and they vanish, no edit to `apphost.mts` required. The mechanics are in [Orchestration with Aspire](https://rickylabs.github.io/netscript/explanation/aspire/).

> Regenerate without re-scaffolding
>
> After changing the resource graph, run
>
> netscript generate aspire
>
> from the workspace root. It re-reads
>
> appsettings.json
>
> and regenerates the TypeScript AppHost helpers; it does not replace the rest of the scaffold. Service mutation verbs regenerate those helpers in the same operation:
>
> ```
> netscript service ref add api users
> netscript service ref remove api users
> netscript service set api --port 3010 --enabled true
> ```

## Step 2 — Restore the AppHost SDK (once)

The AppHost runs on its own isolated Node runtime inside `aspire/` so its dependency graph never leaks into your Deno workspace. Restore that runtime once per machine (and after an SDK bump):

```bash
# Run from inside the aspire/ folder. One-time SDK restore.
cd aspire
aspire restore
```

## Step 3 — Start the resource graph

A single `aspire start` translates `appsettings.json` plus the plugin contributions into a coherent resource graph and boots all of it — infrastructure first, then services, plugin APIs, and background processors, with cross-references resolved into injected environment variables:

```bash
# Still inside aspire/. Boots the whole graph; prints the dashboard URL + a login token.
aspire start
```

When boot finishes, `aspire start` prints the dashboard address and a one-time login token. The graph a single run stands up:

**What `aspire start` brings up (the local resource graph)**

| Name | Type | Description |
| --- | --- | --- |
| `aspire (dashboard)` | `https://localhost:18888 / http://localhost:18889` | The Aspire dashboard. Live resource list, console logs, structured logs and traces. A login token is printed on start. |
| `OTLP collector` | `http://localhost:4318` | OpenTelemetry endpoint (http/protobuf) the dashboard runs; framework spans and structured logs land here automatically. |
| `postgres` | `Container` | Provisioned via Docker. The database `netscript db` commands target — reachable only once Aspire is up. Postgres is the recommended engine — pass `--db postgres` at init; `--db mysql` or `--db mssql` select those engines (each also an Aspire container), or `--db sqlite` for a file-backed database with no container. |
| `redis` | `Container (cache)` | Redis cache — the default `--cache-backend`; Redis-compatible. Backs KV/queue workloads for the runtime plugins. Swap it for `garnet` (also a container) or app-level `deno-kv` via `--cache-backend`. |
| `users (example service)` | `:3000+ (SERVICE range, OS-allocated from 3000)` | The scaffolded oRPC service, when you init with --service. OpenAPI at /api/v1/users/* and RPC at /api/rpc/*. |
| `plugin APIs` | `:8091–8099 (PLUGIN_API range)` | Each installed runtime plugin's HTTP API (workers, sagas, triggers, auth). Ports are allocated from the plugin range, not hardcoded. |
| `background processors` | `executables (no port)` | Each plugin's isolated runners (workers, sagas, triggers) — separate processes, not threads inside the API. |
| `dashboard (Fresh app)` | `:8010` | The scaffolded Fresh frontend, when present (the app range start 8000 + 10). |

> Plugin API ports are range-allocated, not fixed
>
> The runtime plugins publish their APIs from the
>
> :8091–8099
>
> PLUGIN_API
>
> range, and services from the
>
> :3000–3099
>
> SERVICE
>
> range. The conventional assignments (workers
>
> :8091
>
> , sagas
>
> :8092
>
> , triggers
>
> :8093
>
> , auth
>
> :8094
>
> ) are what a default four-plugin workspace lands on — but the dashboard's resource list is the authority for the exact port each resource bound, not a memorized number. Read it from there.

## Step 4 — Initialize the database (through the running AppHost)

With Aspire up, your database engine is live and the `netscript db` commands can reach it. The resource graph above shows Postgres — the recommended engine and the one these recipes use; if you scaffolded with `--db mysql` / `--db mssql` you get that container instead, and `--db sqlite` gives a file-backed database with no Aspire container. Run the commands from the **workspace root** (a second terminal — leave `aspire start` running in the first):

```bash
# From the workspace root, with `aspire start` still up in another terminal.
netscript db init --name init   # create + apply the first migration
netscript db generate           # generate the Prisma client
netscript db seed               # optional: seed development data
```

These talk to the Postgres container Aspire provisioned. Outside Aspire (an isolated CI container, say), point them at your own database via `POSTGRES_URI` / `DATABASE_URL` instead — covered in

[Database & migration](https://rickylabs.github.io/netscript/netscript/data-persistence/how-to/database-migration/) .

## Step 5 — Use the dashboard

Open `https://localhost:18888`, paste the login token `aspire start` printed, and you have a single pane over the running graph:

**Aspire dashboard surfaces**

| Name | Type | Description |
| --- | --- | --- |
| `Resources` | `tab` | Every container and executable above with status, endpoints, and the resolved environment. The authority for which port each resource bound. |
| `Console logs` | `tab` | stdout/stderr per resource — a failing background processor is one click away, not buried in a terminal. |
| `Structured logs + Traces` | `tab` | Spans and structured logs your handlers emit, correlated by traceparent across services. Aspire collects them via the OTLP endpoint at :4318. |

Because Aspire starts each resource with its `OTEL_SERVICE_NAME` and an OTLP endpoint pointed at `http://localhost:4318`, framework-level spans (job dispatch, job execution, scheduler runs) surface here with no extra wiring. See [Observability](https://rickylabs.github.io/netscript/netscript/explanation/observability/)

for the framework-vs-scaffold span boundary.

## In-production pitfalls

> Aspire is the LOCAL story — not a production deployer
>
> aspire start
>
> exists to make
>
> git clone
>
> → one command produce a complete, observable, correctly-wired stack on
>
> one machine
>
> . The Postgres and Redis it starts are throwaway Docker containers for dev convenience —
>
> not
>
> your production database or cache. For a remote target you point processes at managed infrastructure and let your platform own lifecycle; that is the
>
> Deploy
>
> recipe.

> Footguns when `aspire start` will not boot
>
> - **Docker not running.** Aspire provisions Postgres + Redis through Docker; no daemon means the default local workflow cannot start. Start Docker, or use the `--no-aspire` path with your own infrastructure.
> - **Wrong directory.** `aspire restore` and `aspire start` run from inside `aspire/`. `netscript db` commands run from the **workspace root**. Mixing them up is the most common first-run error.
> - **db command before `aspire start`.** Every `netscript db` command needs a live Postgres. Run it with no Aspire up and it fails fast — bring the graph up first.
> - **Ports in use.** The dashboard wants `:18888`/`:18889` and OTLP `:4318`; services and plugin APIs claim the `:3000+` and `:8091–8099` ranges. A stale prior run holding a port blocks boot — check the dashboard resource list (or your process table) and free it.
> - **The AppHost is Node, your app is Deno.** The two runtimes are isolated on purpose; an `aspire restore` failure is a Node/SDK problem in `aspire/`, not a Deno workspace problem.

## See also

- **Why it works this way:** [Orchestration with Aspire](https://rickylabs.github.io/netscript/netscript/explanation/aspire/)

  — the AppHost, plugin contributions, two-pass reference resolution, and the dashboard.
- **Ship it remotely:** [Deploy](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/how-to/deploy/) — the production companion: deployable units, backing services, and the `--no-aspire` path.
- **The database sequence:** [Database](https://rickylabs.github.io/netscript/netscript/data-persistence/database/) and

[Database & migration](https://rickylabs.github.io/netscript/netscript/data-persistence/how-to/database-migration/) .

- **Exact symbols + full port map:** [the Aspire reference](https://rickylabs.github.io/netscript/netscript/reference/aspire/)

  and the [CLI reference](https://rickylabs.github.io/netscript/netscript/cli-reference/) .

[Graceful shutdown](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/how-to/graceful-shutdown/) [How-to guides](https://rickylabs.github.io/netscript/netscript/how-to/)
