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, and the
Aspire AppHost that orchestrates them locally. It does not generate a Dockerfile, a
docker-compose.yml, or a cloud target for you. Those are yours to add, and this page shows
you exactly which verified facts to build them from.
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 and the Storefront tutorial. Then confirm the workspace is healthy locally before you try to move it:
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 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:
Postgres (the recommended default 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.
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.
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 add. 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:
| 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 5. |
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 (raw Hono routes, not oRPC). 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). |
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.
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.
# From the workspace root — prove the graph is healthy before you ship it.
deno task check
deno task lint
deno task test
# 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
# 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
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.
| 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). |
Step 4 — Choose an orchestration path
This is the real fork in the road. Pick based on whether your target understands Aspire.
# 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 http://localhost:18888
# Aspire 13.x can also publish a deployment manifest from the same AppHost
# (e.g. `aspire publish`). The scaffold wires the AppHost graph; it does NOT
# pre-select a cloud target for you — you point publish at your platform.
# 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
Step 5 — 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:
# 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:
# 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).
Step 6 — 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):
| 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) |
http://localhost:18888 |
Aspire dashboard: every resource, health, logs, distributed traces (Aspire path only). |
# 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
For the full generated API of each deployable unit, see the reference: workers, sagas, triggers, and streams. For every CLI command grouped by workflow, see the CLI reference.