# Deploy locally

You have built every layer of the team workspace: scaffold, an identity layer, isolated team data, a provisioning job that never blocks the caller, and routes that fail closed. This final chapter runs the whole thing as one coherent system — the point where the app stops being a set of chapters and becomes something a team could actually sit inside. A single `aspire start` stands up Postgres, the Redis cache, your `workspace` service, the `auth-api` service on `:8094`, the Workers API, and every background processor — all wired together and visible in one dashboard. It is the **local** story, and this chapter is precise about exactly that: a complete observable stack on one machine, not a production deployer.

The one-command reach is the differentiator, and you hand-wrote none of it: `netscript init` generated the AppHost in Chapter 1, each plugin you installed since — `auth`, then `workers` — contributed its own service and processor to the resource graph, and one `aspire start` boots the whole thing with cross-references resolved into injected environment. Scaffold to a running, observable stack, with no orchestration wiring you maintain by hand.

1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/01-scaffold/)
2. [2 · Auth](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/02-auth/)
3. [3 · Workspace data](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/03-workspace-data/)
4. [4 · Provision job](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/04-provision-job/)
5. [5 · Route authz](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/05-route-authz/)
6. [6 · Deploy](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/06-deploy/)

## What you will build

Your complete `my-workspace/` running under one Aspire AppHost: every service, plugin API, background processor, and backing container in a single resource graph, observable from the dashboard on `:18888` — including the `:8094` auth service you added in chapter 2. By the end you can read the whole running topology, including the exact port each resource bound, from one pane.

## Before you begin

You need everything from chapters 1–5: the scaffolded workspace, the `auth` plugin, the second database, the workers plugin and `provision-member` job, and the guarded `workspace` service. The AppHost was scaffolded by `netscript init` in chapter 1 (you did not pass `--no-aspire`). Confirm the orchestration entry point is on disk and the workspace type-checks:

```sh
# From the workspace root
ls aspire/apphost.mts aspire/aspire.config.json
deno task check
```

> The order is: scaffold → orchestrate → database
>
> Aspire is
>
> step 2
>
> , before any database command.
>
> netscript init
>
> wrote the
>
> aspire/
>
> AppHost;
>
> aspire start
>
> provisions Postgres and Redis and starts every process;
>
> only then
>
> do
>
> netscript db
>
> commands work, because they migrate
>
> through
>
> the running AppHost. The graph is derived from your installed plugins — adding the
>
> auth
>
> and
>
> workers
>
> plugins in earlier chapters is exactly why their services and processors now appear in it.

## Step 1 — 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):

```sh
cd aspire
aspire restore
```

## Step 2 — Start the whole graph

A single `aspire start` translates `appsettings.json` plus your 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:

```sh
# 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. This is the graph your complete app stands up:

**The local resource graph aspire start brings up**

| Name | Type | Description |
| --- | --- | --- |
| `aspire (dashboard)` | `https://localhost:18888` | 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; framework spans (job dispatch/execution, scheduler runs) land here automatically. |
| `postgres` | `Container` | The primary datasource — the auth.prisma migration from chapter 2 lives here. |
| `workspace (second db)` | `Container` | The isolated workspace datasource from chapter 3, provisioned alongside the primary. |
| `redis` | `Container (cache)` | Redis cache — the default `--cache-backend`; Redis-compatible. Backs KV/queue workloads and the kv-oauth session store. |
| `workspace (service)` | `:3001` | Your guarded oRPC service from chapter 5 (note: this tutorial assumes port 3001; in unpinned scaffolds, each project is allocated its own randomized high-range ports) — /api/workspace requires a scoped principal, /health stays public. |
| `auth-api` | `:8094 (pinned in chapter 2)` | The auth plugin's service from chapter 2 — /api/v1/auth/* (signin, callback, signout, session, me). |
| `workers-api` | `allocated port` | The Workers API from chapter 4 — triggers and inspects the provision-member job. |
| `background processors` | `executables (no port)` | The workers processor that drains the job queue — a separate process, not a thread in the API. |

> Only pinned ports are predictable
>
> Two host ports here are predictable because
>
> you
>
> pinned them:
>
> workspace
>
> on
>
> :3001
>
> (chapter 1's
>
> --service-port
>
> ) and
>
> auth-api
>
> on
>
> :8094
>
> (chapter 2's
>
> --port
>
> ). The Workers API you installed without a
>
> --port
>
> , so the installer chose its host port for you — deterministically, from a hash of your project name over the IANA dynamic range
>
> 49152–65535
>
> , then probing upward past ports already taken
>
> in this workspace
>
> . That spreads projects apart well enough to be practical, but it is not a guarantee: the range is finite, workspaces do not see each other's allocations, and any pin can land on top of one. Read the actual number from the dashboard's resource list rather than assuming.

## Step 3 — Use the dashboard

Open `https://localhost:18888`, paste the login token `aspire start` printed, and you have one 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 auth-api or workers processor is one click away. |
| `Structured logs + Traces` | `tab` | Spans and structured logs correlated by traceparent across services — collected via the OTLP endpoint at :4318. |

## Verify your progress

With the graph up, walk the whole app end to end — the auth service, the guarded route, and the job:

```sh
# Auth service is up (chapter 2)
curl http://localhost:8094/health/ready

# The guarded route rejects an anonymous caller (chapter 5)
curl -i http://localhost:3001/api/workspace            # 401 UNAUTHORIZED

# ...and allows a correctly-scoped one
curl -i -H 'authorization: Bearer read' http://localhost:3001/api/workspace   # 200

# The Workers API is live (chapter 4)
curl <workers-endpoint>/api/v1/workers/jobs            # provision-member appears
```

- [ ] `aspire restore` and `aspire start` succeed from inside `aspire/`.
- [ ] The dashboard on `:18888` lists `postgres`, `workspace` (db), `redis`, `workspace` (service), `auth-api`, and `workers-api` — all green.
- [ ] `curl http://localhost:8094/health/ready` succeeds.
- [ ] An anonymous `GET /api/workspace` returns `401`; `Bearer read` returns `200`.
- [ ] `GET /api/v1/workers/jobs` lists `provision-member`.

> 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, and the
>
> kv-oauth
>
> session store and auth credentials here are local-dev values. 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 (and the second workspace db) through Docker; no daemon means the happy path does not start.
> - **Wrong directory.** `aspire restore` and `aspire start` run from inside `aspire/`; `netscript db` commands run from the workspace root.
> - **db command before aspire start.** Every `netscript db` command needs a live Postgres — bring the graph up first.
> - **Ports in use.** The dashboard wants `:18888`/`:18889` and OTLP `:4318`; your pinned `workspace` and `auth-api` claim `:3001` and `:8094`, and the remaining plugin runtimes claim allocated ports in `49152–65535`. A stale prior run holding a port blocks boot.

## What you built

The complete authenticated team-workspace backend, running locally as one orchestrated system: Postgres, an isolated workspace database, Redis, the guarded `workspace` service, the `auth-api` auth service on `:8094`, and the Workers API and its processor — all in one Aspire resource graph, observable from one dashboard. You walked the whole arc: a pluggable auth backend, a session, team data on its own catalog datasource, off-path provisioning, and a route-authz seam that fails closed — single-tenant by design, with org scoping an explicit app-level extension. The off-boarded contractor gets a `401`; the engineer you paged gets provisioned without anyone waiting on the write.

## Where to go next

- **Ship it remotely** → [Deploy](https://rickylabs.github.io/netscript/orchestration-runtime/how-to/deploy/) — the production companion: deployable units, managed backing services, and the `--no-aspire` path.
- **Go deeper on auth** → [Authentication capability](https://rickylabs.github.io/netscript/capabilities/auth/) and [The authentication model](https://rickylabs.github.io/netscript/explanation/auth-model/).
- **Understand the orchestrator** → [Orchestration with Aspire](https://rickylabs.github.io/netscript/explanation/aspire/).
- **Browse more recipes** → the [how-to guides](https://rickylabs.github.io/netscript/how-to/).

[5 · Route authz](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/05-route-authz/) [How-to guides](https://rickylabs.github.io/netscript/netscript/how-to/)
