# Deploy a NetScript workspace

**Goal:** take the workspace you scaffolded with `netscript init` and run it somewhere other than your laptop — a container host, a VM, or a managed platform — with clear expectations about what the scaffold wires for you and what you still own.

This is a task recipe, not a one-click button. NetScript is in alpha, and the scaffold is deliberately minimal about deployment: it gives you a single declarative description of every process (`appsettings.json`), runnable Deno entrypoints with explicit permissions, the Aspire AppHost that orchestrates them locally and can publish deployment artifacts for targets you configure in it, and starter GitHub Actions workflows for the shipped deployment targets. The CLI has thin deploy routers for Deno Deploy, Docker/Compose, Kubernetes, Azure, and Cloud Run, but it does **not** generate a `Dockerfile`, a `docker-compose.yml`, or a finished cloud infrastructure stack for you — target-specific infrastructure still lives in Aspire and your cloud account, assembled from the verified facts below.

> What is wired vs. what is manual
>
> Wired:
>
> a declarative resource graph in
>
> appsettings.json
>
> (ports, entrypoints, permissions, env vars, dependencies), runnable per-process Deno entrypoints, and the Aspire AppHost (
>
> aspire/apphost.mts
>
> ) for local orchestration.
>
> Generated CI starters:
>
> .github/workflows/deploy-compose-ghcr.yml
>
> ,
>
> .github/workflows/deploy-deno-deploy.yml
>
> , and
>
> .github/workflows/deploy-bare-metal.yml
>
> .
>
> Delegated:
>
> Docker/Compose, Kubernetes, and Azure target commands call
>
> aspire publish
>
> ,
>
> aspire deploy
>
> , and
>
> aspire destroy
>
> against the generated AppHost code. Aspire emits the manifests and provisions supported resources; your cloud account, cluster credentials, registry access, and RBAC remain yours. Cloud Run follows the Docker-image provider lane: Docker builds and pushes the image, then
>
> gcloud run deploy
>
> applies it.
>
> Manual:
>
> there is
>
> no
>
> generated container image, compose file, or finished cloud infrastructure stack.
>
> netscript.config.ts
>
> ships an empty
>
> deploy: {}
>
> block unless you configure a target. You assemble the production target yourself from the primitives below — every one of which is a verified fact you can copy verbatim.
>
> Migration (#337):
>
> Windows deploy settings now live under
>
> deploy.targets.windows
>
> (previously
>
> deploy.windows
>
> ).

> Generated CI follows the shipped targets
>
> The scaffolded workflows cover the deploy surfaces that ship today:
>
> Deno Deploy
>
> (
>
> netscript deploy deno-deploy up
>
> ),
>
> Compose/GHCR
>
> (
>
> netscript deploy compose plan
>
> +
>
> netscript deploy docker up
>
> ), and
>
> bare metal
>
> (
>
> netscript deploy build
>
> ). They are intentionally starter pipelines: fill in repository secrets, GitHub environment protection, host credentials, and target-specific configuration before you treat them as production release jobs. See
>
> Deploy to Deno Deploy
>
> for the managed-platform command reference.

> Managed deployment commands
>
> There is now a first-class, runnable managed-platform path:
>
> `netscript deploy deno-deploy <op>`
>
> (
>
> plan
>
> /
>
> up
>
> /
>
> status
>
> /
>
> logs
>
> /
>
> down
>
> ) pushes a workspace to
>
> Deno Deploy
>
> over the native
>
> deno deploy
>
> CLI, with a preflight guard and
>
> deploy.targets['deno-deploy']
>
> config. Aspire-backed targets are also routed:
>
> netscript deploy docker|compose|kubernetes|azure-aca|azure-app-service|azure-aks|cloud-run <op>
>
> delegates to the target adapter. See
>
> Deploy to Deno Deploy
>
> for Deno Deploy details.

## Before you start

You need a working, type-checked workspace and a clear idea of where it is going. If you have not built one yet, start with [Quickstart](https://rickylabs.github.io/netscript/quickstart/) and the [Storefront tutorial](https://rickylabs.github.io/netscript/tutorials/storefront/). Then confirm the workspace is healthy locally before you try to move it:

```bash
deno task check    # type-check apps, services, contracts
deno task lint
deno task test
```

For the orchestration model that underpins everything below — why the AppHost lives in its own `aspire/` folder, and how it provisions Postgres and Redis — read the [Aspire explanation](https://rickylabs.github.io/netscript/explanation/aspire/) alongside this recipe.

## The mental model: three layers

A NetScript deployment is three layers, and you choose how much of each you keep in production:

1. Backing services

Postgres (the recommended database; `mysql`, `mssql`, or `sqlite` are first-class alternatives via `--db`) and Redis (KV/cache — the default `--cache-backend`; `garnet` and `deno-kv` are alternatives). In dev, Aspire provisions Postgres/MySQL/SQL Server as containers (`sqlite` is file-backed, no container) and Redis as a container. In production you bring your own — managed database, managed Redis-compatible cache.

2. NetScript processes

Each API service, plugin service, background processor, and the Fresh app is one Deno process with an entrypoint, a port, and an explicit permission set — all declared in appsettings.json.

3. Orchestration

Aspire's AppHost wires the graph together locally. In production you can keep Aspire (it can publish a deployment manifest) or drop it and run each process yourself with your own supervisor.

## Step 1 — Know your deployable units (`appsettings.json`)

`appsettings.json` is the single source of truth for *what runs*. The CLI writes it during `netscript init` and updates it as you `netscript plugin install`. Every Aspire resource — and every process you would deploy by hand — is described there. From a workspace with the four first-party plugins installed, the graph looks like this:

**Resources declared in appsettings.json (verified from a scaffolded workspace)**

| Name | Type | Description |
| --- | --- | --- |
| `users` | `service · :3000 (the scaffold default; the exact port is OS-allocated from the SERVICE range starting at 3000)` | Example oRPC service. Entrypoint `src/main.ts`, runtime `deno`. RPC mounts under `/api/rpc/*`. |
| `streams` | `plugin · :4437` | durable-streams runtime service. `RequiresDb=false`, `RequiresKv=false`. Real producer runtime — see Step 6. |
| `workers-api` | `plugin · :8091` | Workers API. Requires DB + KV. References `streams`. |
| `sagas-api` | `plugin · :8092` | Sagas API. Requires DB + KV. References `workers-api`, `streams`. |
| `triggers-api` | `plugin · :8093` | Triggers API. Typed v1 oRPC contract for trigger/event introspection + management; the webhook ingress endpoint `POST /api/v1/webhooks/:triggerId` stays a raw HMAC-verifying route by design. Requires DB + KV. References `workers-api`, `streams`. |
| `workers / sagas` | `background processor` | Entrypoint `bin/combined.ts`. Watch mode + telemetry on. Workers runtime pool via `WORKERS_CONCURRENCY`; sagas via `SAGA_CONCURRENCY`. |
| `triggers` | `background processor` | Entrypoint `src/runtime/trigger-processor.ts`. Concurrency 10 via `TRIGGER_CONCURRENCY`. |
| `dashboard` | `app · :8010` | Fresh frontend. References service `users`. |
| `postgres / redis` | `infrastructure` | `Mode=Container` in dev. `PrimaryDatabase=postgres`, `PrimaryCache=redis` (the default; `garnet` via `--cache-backend garnet`). |

> Auth service is opt-in (port :8094)
>
> If you add the auth plugin, a fifth API service —
>
> auth-api
>
> on
>
> :8094
>
> — joins the graph. It is an oRPC service exposing five endpoints under
>
> /api/v1/auth/{signin,callback,signout,session,me}
>
> , backed by one active backend selected via
>
> NETSCRIPT_AUTH_BACKEND
>
> (default
>
> kv-oauth
>
> ). Treat it as just another deployable unit: it has an entrypoint, a port, and a permission set in
>
> appsettings.json
>
> like every other process.

Each entry carries the exact information a deploy needs: `Runtime`, `Port`, `Entrypoint`, `Workdir`, `RequiresDb`/`RequiresKv`, the Deno `Permissions` array, the concurrency env var, and `PluginReferences` (the wiring order). When you containerize or write systemd units, copy these values verbatim — do not guess them.

> Permissions are deployment-critical
>
> The
>
> Permissions
>
> array on each resource is the exact Deno permission set that process needs — for example
>
> workers-api
>
> runs with
>
> --unstable-kv --allow-net --allow-env --allow-read --allow-write --allow-run
>
> , while
>
> sagas-api
>
> and
>
> triggers-api
>
> use
>
> --unstable-kv --allow-all
>
> . Reproduce these flags exactly in your run command; a missing
>
> --unstable-kv
>
> will crash any DB/KV-backed process at startup.

## Step 2 — Build and validate a release artifact

There is no opinionated build step that produces a single bundle — each Deno process runs from source. Your "build" is therefore: cache dependencies, type-check, and (optionally) pre-generate the database client and plugin registries so the container does not do it at boot.

```bash
# From the workspace root — prove the graph is healthy before you ship it.
deno task check
deno task lint
deno task test
```

```bash
# Warm the module cache so production start-up does no network fetch.
# Cache each deployable entrypoint you intend to run.
deno cache services/users/src/main.ts
deno cache plugins/workers/services/src/main.ts
deno cache plugins/sagas/services/src/main.ts
deno cache plugins/triggers/services/src/main.ts
```

```bash
# Requires Aspire (and therefore Postgres) up first — see the DB callout below.
# Bake the Prisma client + plugin registries into the artifact so boot is deterministic.
netscript db generate
netscript generate plugins
```

> Aspire is step 2: Postgres comes up before any db command
>
> Every
>
> netscript db <cmd>
>
> talks to a live Postgres. In the scaffold that Postgres is provisioned
>
> by Aspire
>
> . So the order is always:
>
> cd aspire && aspire start
>
> (which brings Postgres and Redis up)
>
> before
>
> any
>
> netscript db init
>
> /
>
> db generate
>
> /
>
> db seed
>
> . Running a DB command with no Postgres reachable — for example in an isolated CI container — fails fast. In production, point the same commands at your managed Postgres via
>
> POSTGRES_URI
>
> or
>
> DATABASE_URL
>
> instead of relying on Aspire. See
>
> Database migration
>
> for the full sequence.

## Step 3 — Provision backing services

In production you do **not** run Postgres and Redis as throwaway Aspire containers. You provision them as durable, managed resources and hand their connection details to NetScript through environment variables. NetScript reads the database URL from `POSTGRES_URI` (falling back to `DATABASE_URL`) and normalizes engine-specific connection strings to a URL — this is handled in `database/postgres/prisma.config.ts`.

**Production environment a NetScript deployment expects**

| Name | Type | Description |
| --- | --- | --- |
| `POSTGRES_URI` | `string (url)` | Primary Postgres connection. `DATABASE_URL` is the accepted fallback. Read by Prisma config. |
| `REDIS_URI / cache url` | `string` | Redis-compatible cache endpoint for the `redis` KV/cache resource (the default backend). With `--cache-backend garnet` the key is `GARNET_URI` for the `garnet` resource (managed Redis or Garnet in prod). |
| `PORT` | `number` | Per-process listen port. Each service reads it (e.g. `Deno.env.get('PORT') ?? '8091'`) and falls back to its default. |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `string (url)` | OTLP collector. Dev defaults to `http://localhost:4318` (http/protobuf) via the Aspire dashboard. |
| `NETSCRIPT_SAGA_STORE` | `kv \| prisma` | Durable saga store backend (mandatory when sagas run). Also settable via appsettings `sagas.store.backend`. |
| `NETSCRIPT_AUTH_BACKEND` | `string` | Active auth backend if the auth plugin is installed. Default `kv-oauth`. |
| `WORKERS_CONCURRENCY` | `number` | Workers runtime process pool size. Current Aspire metadata also emits `WORKER_CONCURRENCY`, but the runtime honors `WORKERS_CONCURRENCY`; set the runtime var. |
| `SAGA_CONCURRENCY` | `number` | Sagas background processor concurrency (default 2). |
| `TRIGGER_CONCURRENCY` | `number` | Triggers background processor concurrency (default 10). |

> appsettings.json is your config map
>
> The
>
> Otel.HttpEndpoint
>
> ,
>
> Databases.postgres
>
> ,
>
> Cache.redis
>
> (or
>
> Cache.garnet
>
> when scaffolded with
>
> --cache-backend garnet
>
> ), and per-process
>
> ConcurrencyEnvVar
>
> values in
>
> appsettings.json
>
> tell you every knob to externalize. Treat that file as the contract between your infrastructure and the NetScript processes.

> Queue and saga durability backends are deploy-time choices
>
> Two stateful subsystems pick a backend at deploy time, so decide before you ship:
>
> - **Queue** has four backends — Deno KV, Redis, RabbitMQ, and PostgreSQL. Auto-discovery probes RabbitMQ → Redis → Deno KV only; **PostgreSQL is explicit-provider only** (`provider: 'postgres'` with `connection.postgres.{url, tableName}`).
> - **Sagas** persist durable runtime state to `kv` or `prisma`, selected by `NETSCRIPT_SAGA_STORE` (or appsettings). The selection is mandatory — the runtime throws if it is unset. The Prisma path writes the `saga_runtime_*` tables.
>
> See
>
> Queues & cron
>
> and
>
> Durable sagas
>
> for the full backend matrices.

## Step 4 — Choose an orchestration path

This is the real fork in the road. Pick based on whether your target understands Aspire.

```bash
# The AppHost is a TypeScript/Node project under aspire/ (apphost.mts).
# Locally it provisions Postgres + Redis and wires every process.
cd aspire
aspire restore   # one-time: restore the TS AppHost SDK
aspire start       # boots the full graph; dashboard at https://localhost:18888

# Aspire 13.x can publish/deploy target artifacts from the same AppHost.
netscript deploy kubernetes plan --project-root .
netscript deploy kubernetes up --project-root .
```

```bash
# Scaffold without the orchestration layer, then run processes yourself.
netscript init my-app --no-aspire

# You now own provisioning Postgres + a cache, and starting each process.
# Bring-your-own-supervisor: systemd, a container per process, or a PaaS.
# Start the Fresh app directly during dev:
deno task --cwd apps/dashboard dev
```

> The AppHost is Node/TypeScript, not dotnet
>
> aspire/apphost.mts
>
> is a generated
>
> TypeScript/Node
>
> AppHost (language
>
> typescript/nodejs
>
> , Aspire SDK 13.x, package
>
> Aspire.Hosting.PostgreSQL
>
> ). It is intentionally isolated in
>
> aspire/
>
> so its Node dependency graph never leaks into the Deno root.
>
> netscript.config.ts
>
> records this same path under
>
> aspire.appHost
>
> (
>
> 'aspire/apphost.mts'
>
> ), so the config metadata and the artifact you actually run agree. The dashboard binds
>
> :18888
>
> and the OTLP collector listens on
>
> :4318
>
> .

### Aspire cloud targets

The Aspire-backed target routers all share the same shape:

```bash
# Generate artifacts without applying them.
netscript deploy kubernetes plan --project-root . --output-dir .deploy/kubernetes

# Apply/provision through the validated AppHost platform integration.
netscript deploy azure-aks up --project-root . --output-dir .deploy/azure-aks

# Tear down the previously deployed target.
netscript deploy azure-aks down --project-root . --output-dir .deploy/azure-aks
```

For Kubernetes, add the Aspire Kubernetes integration to the TypeScript AppHost (`aspire add kubernetes`, then `builder.addKubernetesEnvironment('k8s')`). Use `publishAsKubernetesService(...)` in `aspire/apphost.mts` for per-service manifest customization such as replicas, labels, annotations, or extra manifests. `plan` runs `aspire publish --apphost <path> --output-path .deploy/kubernetes`; Aspire emits a Helm chart with `Chart.yaml`, `values.yaml`, and `templates/`.

Apply the published chart with your normal cluster workflow:

```bash
kubectl config current-context
helm upgrade --install my-netscript-app .deploy/kubernetes

# Or inspect/apply rendered manifests if your release process requires kubectl.
helm template my-netscript-app .deploy/kubernetes > .deploy/kubernetes/rendered.yaml
kubectl apply -f .deploy/kubernetes/rendered.yaml
```

For Azure, configure the matching AppHost hosting integration before using the router. The CLI validates the AppHost source contains an Azure Container Apps, Azure App Service, or Azure Kubernetes marker before it shells `aspire publish|deploy --apphost <path>`. Azure CLI login, subscription/location parameters, provider feature registration, and RBAC are operator prerequisites.

For Cloud Run, configure the Docker-image provider fields:

```ts
export default defineConfig({
  // ...
  deploy: {
    targets: {
      'cloud-run': {
        registry: 'us-docker.pkg.dev/acme/prod',
        imageName: 'orders-api:latest',
      },
    },
  },
});
```

`netscript deploy cloud-run up` runs `docker build -t <registry>/<imageName> .`, `docker push <registry>/<imageName>`, then `gcloud run deploy <service> --image <registry>/<imageName> --quiet`.

## Step 5 — Wire generated CI and promotion

New Aspire-backed workspaces include three GitHub Actions starter workflows under `.github/workflows/`:

**Generated deployment workflows**

| Name | Type | Description |
| --- | --- | --- |
| `deploy-compose-ghcr.yml` | `Compose + GHCR` | Restores the Aspire AppHost, emits Compose output with `netscript deploy compose plan`, builds/pushes images to GHCR, then runs `netscript deploy docker up` with `--clear-cache`. |
| `deploy-deno-deploy.yml` | `Deno Deploy` | Runs workspace checks, then calls `netscript deploy deno-deploy up` using GitHub secrets and variables for the Deno Deploy token, organization, and app. |
| `deploy-bare-metal.yml` | `Bare metal` | Compiles service artifacts with `netscript deploy build` on Linux and Windows runners, then uploads the output as workflow artifacts for host-specific installation. |

Treat the workflow `environment` input as the promotion ladder: `development` first, then `staging`, then `production`. In GitHub, map those names to protected environments and keep secrets environment-scoped so a staging run cannot accidentally read production credentials. Promote the same reviewed commit through each environment; do not rebuild from a different branch between staging and production.

> Do not persist Aspire deployment state in CI
>
> Aspire deployment state is cached under
>
> ~/.aspire/deployments/{AppHostSha}/{environment}.json
>
> and may contain plaintext values entered during deployment prompts. The generated Compose/GHCR workflow does not cache
>
> ~/.aspire/deployments
>
> and passes
>
> --clear-cache
>
> to
>
> netscript deploy docker up
>
> , which forwards it to
>
> aspire deploy --clear-cache
>
> . Keep that behavior in CI. If you need durable values, store them in GitHub environment secrets or your platform's secret manager, not in the Aspire deployment cache.

## Step 6 — Run a process by hand (the bare-metal primitive)

Under every option above, the atomic unit is the same: one Deno process started from an entrypoint with the exact permission set from `appsettings.json`. This is what a container `CMD`, a systemd `ExecStart`, or a PaaS start command ultimately becomes. For the workers API (`:8091`), it is:

```bash
# Run from the workspace root. Flags and entrypoint come straight from appsettings.json.
PORT=8091 \
deno run \
  --unstable-kv --allow-net --allow-env --allow-read --allow-write --allow-run \
  plugins/workers/services/src/main.ts
```

The corresponding background processor (which actually executes jobs) runs its own entrypoint:

```bash
# Workers + sagas background processors share bin/combined.ts; triggers uses its own.
WORKERS_CONCURRENCY=2 \
deno run \
  --unstable-kv --allow-net --allow-env --allow-read --allow-write --allow-run \
  workers/bin/combined.ts
```

Map this pattern across every enabled resource and you have a complete, container-free deployment. To containerize, each process becomes one image whose `CMD` is the matching `deno run` line; orchestrate them with compose or your platform of choice, honoring the `PluginReferences` start order (streams → workers → sagas/triggers, plus auth-api when present).

> Limits of the alpha scaffold
>
> - The CLI scaffold does not hand-author a `Dockerfile`, `docker-compose.yml`, or Kubernetes manifest. Docker/Compose, Kubernetes, and Azure artifacts are generated by Aspire from the AppHost code you configure; you write anything Aspire does not emit from the `appsettings.json` facts above.
> - Cloud Run uses the Docker-image provider lane (`docker build`, `docker push`, `gcloud run deploy`) and requires `registry` plus `imageName` in config.
> - `netscript.config.ts` ships an empty `deploy: {}` block unless you configure a target. Generated workflows are starter CI definitions; you still provide registry, host, cloud, and secret-manager configuration before they are production release jobs.
> - Cluster/cloud auth, registry access, RBAC, subscriptions, regions, and provider quotas are operator prerequisites. The CLI shells to Aspire, Docker, or gcloud; it does not create credentials.
> - The `streams` service runs a **real producer runtime** (durable-streams over `@netscript/plugin-streams-core`, served on :4437) — deploy it as a first-class service. What is *not* production-ready is the manifest-helper layer: `@netscript/plugin-streams`'s `defineStreamProducer`/`defineStreamConsumer` **throw `StreamUnsupportedOperationError`** by design (use `@netscript/plugin-streams-core` directly), and there is no in-process consumer `subscribe()` — consumption is HTTP/SSE.
> - The scaffold worker `createJobTools(ctx)` handler helpers (`trace.addEvent`, `withChildSpan`, `progress`) are still no-op stubs (tracked debt, fix planned). Job dispatch/execution traces still appear in Aspire automatically; for custom handler spans call `@netscript/telemetry` helpers directly.
> - DB commands assume a reachable Postgres — in CI/containers without Aspire, inject `POSTGRES_URI` yourself or the command fails fast.

## Step 7 — Verify the deployment

Once your processes are up against real backing services, hit the health endpoints to confirm the graph is wired. These are the exact routes the local runtime exposes (substitute your production host):

**Health and liveness endpoints (verified live)**

| Name | Type | Description |
| --- | --- | --- |
| `GET /health` | `:8091` | Workers API health. |
| `GET /health/live` | `:8092` | Sagas API liveness. |
| `GET /health` | `:8093` | Triggers API health (Hono). |
| `GET /api/v1/workers/jobs` | `:8091` | Lists registered worker jobs — proves the jobs registry generated. |
| `GET /api/v1/sagas/sagas` | `:8092` | Lists registered sagas — proves saga metadata is in KV. |
| `POST /api/v1/webhooks/inbound/generic` | `:8093` | Inbound webhook → enqueues the workers health-check job (end-to-end proof). |
| `GET /api/v1/events?limit=10` | `:8093` | Recent trigger events. |
| `GET /api/v1/auth/session` | `:8094` | Auth session probe (only if the auth plugin is installed). |
| `(dashboard)` | `https://localhost:18888` | Aspire dashboard: every resource, health, logs, distributed traces (Aspire path only). |

```bash
# Smoke a deployed graph (replace localhost with your host).
curl -fsS http://localhost:8091/health
curl -fsS http://localhost:8092/health/live
curl -fsS http://localhost:8093/health
curl -fsS "http://localhost:8091/api/v1/workers/jobs"
```

If every health endpoint returns and `/api/v1/workers/jobs` lists your jobs, the processes are running with their permissions, reaching Postgres/KV, and discovering their registries — the deployment is live.

## Where to go next

[Deploy to Deno Deploy  The one runnable managed-platform path: netscript deploy deno-deploy plan/up/status/logs/down, the unstable-API guard, and deploy.targets['deno-deploy'] config.](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/how-to/deploy-deno-deploy/)

[How Aspire orchestrates it  The AppHost graph, two-pass resource wiring, and why orchestration is a layer you can keep or drop.](https://rickylabs.github.io/netscript/netscript/explanation/aspire/)

[Database migration  Run db init → generate → seed → status against Postgres — and why aspire start comes first.](https://rickylabs.github.io/netscript/netscript/data-persistence/how-to/database-migration/)

[Add OpenTelemetry  Wire spans and traceparent propagation to your OTLP collector in production.](https://rickylabs.github.io/netscript/netscript/observability/how-to/add-opentelemetry/)

For the full generated API of each deployable unit, see the reference: [workers](https://rickylabs.github.io/netscript/reference/workers/), [sagas](https://rickylabs.github.io/netscript/reference/sagas/), [triggers](https://rickylabs.github.io/netscript/reference/triggers/), and [streams](https://rickylabs.github.io/netscript/reference/streams/). For every CLI command grouped by workflow, see the [CLI reference](https://rickylabs.github.io/netscript/cli-reference/).
