# The NetScript architecture

NetScript is a set of Deno-native framework packages, published on [JSR](https://jsr.io) and composed into a running backend by **first-party plugins**, orchestrated locally by **Aspire**. This essay answers one question: when you build an app on NetScript, what are the layers you actually work with, what runs where, and which way do the dependencies point?

![A NetScript workspace as four stacked layers: typed contracts at the base, packages and plugins composed on top, the host wiring plugins through generated registries, and the runtime executing services and background processors — all orchestrated by Aspire.](https://rickylabs.github.io/netscript/netscript/assets/diagrams/architecture-overview.svg)

*The four layers an app author works with: typed contracts, packages and plugins, the host that assembles them, and the runtime that executes services and background processors.*

Read this for orientation, not step-by-step work. The [tutorials]( [Tutorials](https://rickylabs.github.io/netscript/netscript/tutorials/) ) and [how-to guides]( [How-to guides](https://rickylabs.github.io/netscript/netscript/how-to/) ) are the practical paths; the [glossary](https://rickylabs.github.io/netscript/glossary/) defines every term of art; and each layer below routes you onward to the capability hub or reference unit that elaborates it.

> How to read this page
>
> This is a
>
> mental-model
>
> page. It explains
>
> why
>
> NetScript is split the way it is. If you want exact exports, jump to a
>
> reference unit
>
> ; if you want to ship something, start with a
>
> tutorial
>
> .

## The thesis: the published surface is the product

NetScript treats publishing as the whole point. A package's `mod.ts` is not an implementation detail that happens to be exported — it is the contract the framework makes with you, the caller. The shape you see through `deno doc`, the JSR doc score, and the type checker is the surface everything else serves.

Two consequences explain most of the design decisions you will meet:

1. **Types are designed before implementation.** The public types and the README come first; the code that satisfies them comes second. If `deno doc <package>` does not read end-to-end as a manual, the surface is considered broken, regardless of how the internals work.
2. **Simple is preferred over easy at every boundary.** Approachability comes from a small, predictable surface rather than hidden magic. The common 80% case is one chained call; advanced cases unfold one method deeper rather than appearing as required configuration on the entry point.

The deeper rationale for this contract-first stance is the subject of [Contracts & type flow]( [Contracts & type flow](https://rickylabs.github.io/netscript/netscript/explanation/contracts/) ).

## Four layers, one direction of dependency

The single most useful thing to hold in your head is the **direction** of the arrows. Every layer below depends only on the layers beneath it, and **nothing ever edits another boundary's code**. Build that picture once and the rest of the framework stops surprising you.

### 1. Typed contracts — the base, with no dependencies

At the bottom sits the contract layer: schemas and types that describe what a service accepts and returns, with essentially no runtime and no dependencies of their own. A contract is a plain object built from `@orpc/contract` and zod schemas; the same object is imported by the service that *implements* it and by the client that *calls* it, so the two can never drift. Because contracts depend on nothing, everything above can depend on them freely.

This is why "change the shape" is a type error you see at compile time rather than a 500 you discover in production. The contract machinery — `@orpc/contract`, zod, and `implement()` — is the subject of [Contracts & type flow]( [Contracts & type flow](https://rickylabs.github.io/netscript/netscript/explanation/contracts/) ); the API reference is [`@netscript/contracts`]( [contracts](https://rickylabs.github.io/netscript/netscript/reference/contracts/) ).

### 2. Packages and plugins — composing the substrate

Above the contracts sit two kinds of building block:

- **Platform packages** (the `@netscript/*` kernel) know nothing about any one capability. They are the shared substrate: [`config`]( [config](https://rickylabs.github.io/netscript/netscript/reference/config/) ), [`service`]( [service](https://rickylabs.github.io/netscript/netscript/reference/service/) ), [`database`]( [database](https://rickylabs.github.io/netscript/netscript/reference/database/) ), [`queue`]( [queue](https://rickylabs.github.io/netscript/netscript/reference/queue/) ), [`kv`]( [kv](https://rickylabs.github.io/netscript/netscript/reference/kv/) ), [`telemetry`]( [telemetry](https://rickylabs.github.io/netscript/netscript/reference/telemetry/) ), and the rest. A package wraps one concern behind a small surface and never reaches across into another package's internals.
- **Plugins** turn that substrate into a *capability* — workers, sagas, triggers, auth, streams. A plugin **composes** packages; it does not redefine them. It declares its capability by registering named **contributions** (a service, a background processor, a Prisma schema fragment, a stream topic) and leaves the wiring to the host.

The rule that keeps this clean: a plugin depends on packages, packages depend on contracts, and the arrow never reverses. A workers plugin imports the contract types from its sibling [`@netscript/plugin-workers-core`]( [workers](https://rickylabs.github.io/netscript/netscript/reference/workers/) ) package rather than inventing its own; an auth plugin composes the auth seam rather than embedding a provider. The mechanics — public plugin versus core package, manifests, contributions, and the generated registry — live in [The plugin system]( [The plugin system](https://rickylabs.github.io/netscript/netscript/explanation/plugin-system/) ).

> The key insight
>
> A plugin is just a unit that
>
> composes packages
>
> and
>
> registers contributions
>
> . It is never a patch to framework code. Adding a capability is a manifest plus a regenerated registry, not an edit to the kernel. That single rule is what lets the framework grow without the layers tangling.

(You may see the word *archetype* in the doctrine and tooling. That is an internal classification the framework uses for its own quality gates — as an app author you meet it only indirectly, through the shape of a scaffolded plugin. See [The plugin system]( [The plugin system](https://rickylabs.github.io/netscript/netscript/explanation/plugin-system/) ) if you are curious; it is not something you configure.)

### 3. The host — assembling plugins through generated registries

A workspace is more than a pile of plugins; something has to *assemble* them. That is the host's job. When you run the scaffold's generate step, NetScript reads the plugin list from `netscript.config.ts`, validates each plugin's contributions against fixed extension axes, and writes **generated registry** files that wire the contributions together. The host then loads those registries at startup.

The discipline here is the same as everywhere else: the host *reads* plugin manifests and *generates* wiring; it never asks you to hand-edit one plugin to make room for another. Two enabled plugins coexist because the host merged their registrations, not because either was patched. This is why enabling a capability is a config line plus a regenerate, and why the generated files are reproducible output you do not author by hand.

### 4. The runtime — executing services and background processors

At the top, the host's assembled workspace *runs*. NetScript is not one process: it is a small constellation of single-purpose services, each owned by a plugin and each speaking a contract, brought up together by Aspire. Two kinds of thing execute here:

- **Services** answer requests. They are [oRPC services]( [Services & contracts](https://rickylabs.github.io/netscript/netscript/services-sdk/services/) ) mounted at `/api/rpc/*` — including triggers, which serve a typed v1 oRPC contract for trigger and event introspection plus management. The exception is the webhook *ingress* endpoint, a raw signature-verifying route because inbound webhooks are webhook-shaped, not contract-shaped.
- **Background processors** do work outside the request path: the worker job runtime, the durable saga runtime, schedulers, and file watchers. They own state and lifecycle, persist through a store, and shut down cleanly through a `{ stop() }` handle.

The topology below is the canonical shape of a fully-loaded workspace, with every first-party plugin enabled.

```text
                              ┌───────────────────────────────────┐
                              │            Aspire AppHost          │
                              │   dashboard  https://localhost:18888│
                              │  provisions Postgres + Redis (KV)  │
                              └───────────────┬───────────────────┘
                                              │ wires env, ports, resources
            ┌─────────────────────────────────┼─────────────────────────────────┐
            │                                  │                                 │
   ┌────────▼────────┐                ┌────────▼─────────────────────────┐
   │  example service │                │      PLUGIN SERVICES (oRPC)      │
   │   users (service)│                ├──────────────────────────────────┤
   │  /api/rpc/*      │                │  workers (jobs/tasks)            │
   │  oRPC contract   │                │  sagas (durable flows)           │
   └────────┬─────────┘                │  triggers (raw ingress)          │
            │                          │  auth (oRPC /api/rpc)            │
            │ contracts                └────────┬─────────────────────────┘
            │ (@orpc/contract + zod)            │
            │                          ┌────────▼─────────┐
   ┌────────▼──────────────────────────┴──────────────────┴──────────────────────┐
   │                  PACKAGES  (@netscript/* platform substrate)                  │
   │  config · runtime-config · service · contracts · sdk · plugin · database      │
   │  queue · kv · cron · logger · telemetry · aspire                              │
   └────────┬──────────────────────────────────────────────────────────┬──────────┘
            │ produces durable change-data events                       │
   ┌────────▼─────────┐                                      ┌──────────▼─────────┐
   │  streams  (port)  │  durable-stream service             │  Postgres / Redis   │
   │  HTTP / SSE       │  workers · auth · sagas mirror here  │  (relational + KV)  │
   └───────────────────┘                                      └────────────────────┘
```

Read the picture in three bands. **Aspire** sits on top as the local orchestrator: `cd aspire && aspire start` brings up Postgres and Redis (the default cache backend; `garnet` or `deno-kv` are selectable via `--cache-backend`) and starts every service *before* any `netscript db` command runs, with traces, logs, and health landing in the dashboard at `https://localhost:18888`. **Plugin services** sit in the middle, each owning exactly one service on a fixed port; your own scaffolded `users` service sits alongside them, speaking the same contract machinery. **The packages** sit underneath as the shared substrate every plugin composes against. See [Orchestration with Aspire]( [Orchestration with Aspire](https://rickylabs.github.io/netscript/netscript/explanation/aspire/) ) for the control-plane detail.

**Workspace ports and surfaces**

| Surface | Port | Protocol | Owner |
| --- | --- | --- | --- |
| Aspire dashboard | :18888 | HTTP (UI) | Aspire AppHost |
| Example service (users) | Assigned (>= 49152) | oRPC over `/api/rpc/*` | your service |
| Workers | Assigned (>= 49152) | oRPC | `@netscript/plugin-workers` |
| Sagas | Assigned (>= 49152) | oRPC | `@netscript/plugin-sagas` |
| Triggers | Assigned (>= 49152) | oRPC (raw webhook ingress) | `@netscript/plugin-triggers` |
| Auth | Assigned (>= 49152) | oRPC over `/api/rpc/v1/auth/*` | `@netscript/plugin-auth` |
| Streams | Assigned (>= 57344) | durable-stream HTTP / SSE | `@netscript/plugin-streams` |

A subtlety worth fixing early: **triggers serve a typed v1 oRPC contract**, like every other plugin service — the exception is narrower than it looks. Only the webhook *ingress* endpoint (`POST /api/v1/webhooks/:triggerId`) stays a raw, signature-verifying route, because it verifies an HMAC over the raw request bytes from external senders. Service RPC is mounted at `/api/rpc/*` (not `/rpc`). Those two facts trip people up more than any other detail here.

## The pure-backend seam: a port with interchangeable adapters

There is one recurring pattern that, once you see it, you will recognize across the whole framework: the **pure-backend seam**. It is how NetScript keeps a capability open to several implementations without leaking any of them into the contract.

The shape is always the same:

1. A package defines a **port** — a pure interface, no IO — that says what the capability must be able to do.
2. **Adapter** code implements that port against a concrete technology. Adapters are *pure backends*: they depend on the port, never the reverse, and add no public surface of their own beyond the factory you call.
3. A **plugin** composes **exactly one** active adapter, selected by configuration, and exposes it as a running service.

Auth is the most visible instance: a core seam defines an `AuthBackendPort`, three pure adapters implement it (a default interactive KV-OAuth backend, plus WorkOS and Better-Auth), and the auth plugin composes the one backend named by `NETSCRIPT_AUTH_BACKEND`. A capability a given backend cannot honor fails loud through a typed `AuthBackendOperationUnsupportedError` rather than silently doing nothing. The full treatment — the port, the three backends, and the per-backend capability matrix — lives in [The auth model]( [Auth model](https://rickylabs.github.io/netscript/netscript/explanation/auth-model/) ); the capability hub is [Authentication]( [Authentication](https://rickylabs.github.io/netscript/netscript/identity-access/auth/) ).

> The seam is the design, not the adapter
>
> You will see the same port-and-adapter shape everywhere: the
>
> `queue`
>
> defines one port behind four backends (Deno KV, Redis, RabbitMQ, and PostgreSQL), the
>
> `database`
>
> wraps a relational engine behind a port — Postgres (the recommended engine; or
>
> mysql
>
> /
>
> mssql
>
> /
>
> sqlite
>
> via
>
> --db
>
> at scaffold time) — and durable sagas persist through a
>
> kv
>
> or
>
> prisma
>
> store. Auth simply makes the pattern most visible because it ships three adapters in the box.

This is why "swap the backend" is a configuration change in NetScript and not a rewrite: the contract is the port, and adapters are interchangeable behind it.

## Configuration records intent before runtime wiring

NetScript keeps *authored intent* separate from the concrete *runtime wiring* that makes a workspace run. The project-level intent lives in `netscript.config.ts`, authored with [`defineConfig`]( [config](https://rickylabs.github.io/netscript/netscript/reference/config/) ) or `defineConfigAsync`. That object says what the project *is* — its name, paths, enabled plugins, services, apps, database declarations, saga groups, trigger groups, and runtime-config output paths. At startup, `loadConfig` and `initConfig` resolve that authored form into a validated `NetScriptConfig`, and `getConfig` is the synchronous read path after initialization.

The split matters because the host needs both halves: `netscript.config.ts` carries workspace intent such as `paths` and the plugin list (`./plugins/workers/mod.ts`, `./plugins/sagas/mod.ts`, and the rest), while the operational database and resource details are carried by `appsettings.json` and the Aspire AppHost. Plugin packages can contribute partial config fragments through `@netscript/config/merge`, but those fragments merge *into* the already-validated project shape rather than replacing project identity. Runtime overrides are a third layer exposed by [`@netscript/runtime-config`]( [runtime-config](https://rickylabs.github.io/netscript/netscript/reference/runtime-config/) ) for topics such as jobs, sagas, triggers, features, and tasks.

Think of it as three nested layers: `netscript.config.ts` declares project intent, plugin config contributions extend that intent, and appsettings/Aspire/runtime-config materialize the environment-specific wiring. This is also where the capability switches live — the saga durable store (`NETSCRIPT_SAGA_STORE=kv|prisma`), the queue provider, and the active auth backend (`NETSCRIPT_AUTH_BACKEND`) are all selected here rather than in code. It is also why the database docs note a scaffold reality: `databases.config` can be empty in `netscript.config.ts` while the chosen engine — Postgres (the recommended choice), or `mysql` / `mssql` / `sqlite` selected with `--db` at scaffold time — is still provisioned by Aspire (sqlite is file-backed, so it has no Aspire container resource) and described in `appsettings.json`. See [Runtime configuration]( [Runtime configuration](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/runtime-config/) ).

### Naming is part of the contract

Entry points follow fixed verbs, so you can predict behavior from the name alone:

- `defineX(...)` returns a frozen *definition* and does no runtime work.
- `createX(...)` constructs a *runtime* object that owns state and IO.
- `startX(...)` constructs *and* starts a runtime, returning a `{ stop() }` handle.
- `useX(...)` is a hook-style accessor inside a request, render, or handler scope.
- `withX(...)` is a builder method that returns a new builder and never mutates.

When a `mod.ts` would re-export more than roughly twenty symbols, the surface splits into **subpath exports** (for example `@netscript/logger/middleware` and `@netscript/logger/orpc`) to keep bundles lean and isolate adapter-specific code from production exports.

## Durable behavior is modeled as state machines

For the background processors that own *durable* flows, NetScript models them as explicit state machines rather than generic event-handler ladders. [Durable sagas]( [Durable sagas](https://rickylabs.github.io/netscript/netscript/durable-workflows/sagas/) ) are the clearest example: a saga is named phases with explicit correlation, persistence, and compensation, declared through a builder.

```ts
// plugins/sagas/user-registration.saga.ts
import { defineSaga } from "@netscript/plugin-sagas-core";

type State = Readonly<{ status: string }>;

const saga = defineSaga("user-registration")
  .state<State>({ status: "started" })
  .on("UserRegistered", (s, _message, _context) => {
    s.state = { status: "welcoming" };
  })
  .on("WelcomeEmailSent", (s, _message, _context) => {
    s.state = { status: "complete" };
  })
  .build();
```

Failure handling is just as explicit. Handlers throw rich errors; a supervisor — not a scatter of defensive `try/catch` inside the handler — decides whether to restart or escalate, and owns the telemetry for that decision. The durable runtime persists through a selectable `kv` or `prisma` store. The full flow is covered in [The durability model]( [The durability model](https://rickylabs.github.io/netscript/netscript/explanation/durability-model/) ).

## Observability is built into the substrate

Because every plugin composes the same packages, observability is wired once and inherited everywhere. aspire starts an OTLP collector, and the worker job path — dispatch, execution, scheduler, and subprocess continuation — emits **real OpenTelemetry spans automatically**, so job traces appear in the Aspire dashboard without any handler code.

The scaffold `createJobTools(ctx)` surface adds handler events and child spans to that active job trace. Its `progress` helper emits a `job.progress` event and forwards the same update to the workers runtime progress channel.

The full picture is in [Observability]( [Observability](https://rickylabs.github.io/netscript/netscript/explanation/observability/) ).

## The publish gate is the architecture gate

Tests are treated as fitness functions, and the publish gate is where the architecture is *enforced* rather than merely described. `deno publish --dry-run`, `deno doc`, the workspace type checks, the semantic tests, and an export/docs audit are not optional steps — they are the mechanism by which every boundary rule above survives contact with real code. A surface that does not pass them is broken by definition, which is precisely why the published surface can be trusted as the product.

> Alpha package pins
>
> Scaffold output pins exact alpha specifiers such as
>
> jsr:@netscript/plugin-auth-core@0.0.6
>
> .

## Where to go next

- **The contract machinery:** how types flow from a contract to a client in [Contracts & type flow]( [Contracts & type flow](https://rickylabs.github.io/netscript/netscript/explanation/contracts/) ).
- **The plugin mechanics:** public-versus-core split, manifests, and registries in [The plugin system]( [The plugin system](https://rickylabs.github.io/netscript/netscript/explanation/plugin-system/) ).
- **The auth seam in depth:** the port, the three backends, and the capability matrix in [The auth model]( [Auth model](https://rickylabs.github.io/netscript/netscript/explanation/auth-model/) ).
- **Durable behavior:** how sagas and workers persist and recover in [The durability model]( [The durability model](https://rickylabs.github.io/netscript/netscript/explanation/durability-model/) ).
- **The local control plane:** how Aspire provisions everything in [Orchestration with Aspire]( [Orchestration with Aspire](https://rickylabs.github.io/netscript/netscript/explanation/aspire/) ).
- **Exact symbols:** open any package's [reference unit](https://rickylabs.github.io/netscript/reference/).
- **Hands-on:** the [tutorials]( [Tutorials](https://rickylabs.github.io/netscript/netscript/tutorials/) ) and [how-to guides]( [How-to guides](https://rickylabs.github.io/netscript/netscript/how-to/) ).

[Explanation](https://rickylabs.github.io/netscript/netscript/explanation/) [Contracts & type flow](https://rickylabs.github.io/netscript/netscript/explanation/contracts/)
