Skip to main content
Alpha

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:

1. Backing services

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.

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 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:

Resources declared in appsettings.json (verified from a scaffolded workspace)
NameTypeDescription
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.

Production environment a NetScript deployment expects
NameTypeDescription
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):

Health and liveness endpoints (verified live)
NameTypeDescription
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.