Skip to main content
Alpha

Three ideas explain almost everything in NetScript.

The contract is the source of truth, the host is empty until plugins fill it, and one runtime brings up many resources together. Hold these three and the rest of the docs read as detail.

NetScript is a Deno-native backend framework that generates a workspace you own and run yourself. It is not a hosted service and not a single library — it is a coordinated package family plus a CLI that wires the boring seams together. To reason about any part of it, you only need three ideas.

A NetScript workspace: contracts define services; services, workers, sagas, triggers, streams, and auth plugins register against a host; the AppHost materializes each as an Aspire resource alongside Postgres, Redis, and the dashboard.
The whole system in one picture: contracts at the center, plugins contributing to a host, and the AppHost bringing every resource up together.

Idea 1 — The contract is the source of truth

Most type drift in a TypeScript backend comes from having two sources of truth: the shape a service returns, and the shape the client believes it returns. NetScript removes the second one. You define an oRPC contract once; the typed client is derived from it, not generated alongside it. There is no codegen step to fall out of sync, and the type checker becomes your integration test — change the contract and every caller that no longer matches fails to compile.

// One source of truth — imported, not duplicated.
import { baseContract, OffsetPaginationQuerySchema } from '@netscript/contracts';
import { z } from 'zod';

export const usersContract = {
  list: baseContract
    .route({ method: 'GET', path: '/users' })
    .input(OffsetPaginationQuerySchema)
    .output(z.object({ items: z.array(z.object({ id: z.string(), email: z.string() })) })),
};
// `clients.users.list` is typed FROM the contract above — no second definition.
import { defineServices } from '@netscript/sdk';
import { usersContract } from './contracts/users.ts';

const { clients } = defineServices({ users: { contract: usersContract } });

const result = await clients.users.list({ limit: 20, offset: 0 });
console.log(result.items); // fully typed end to end

The same contract that types the client also drives the service router, the OpenAPI document, the Scalar docs, and — because every handler is span-wrapped — the traces. One definition, many consumers, zero hand-maintained duplicates. That is what "the contract is the product" means in practice. Go deeper in contracts & type flow or the services capability hub .

Idea 2 — The host is empty; plugins fill it

A NetScript application is a host that, on its own, does almost nothing. Capabilities — workers, sagas, triggers, streams, auth — arrive as plugins, and each plugin registers what it contributes rather than asking you to edit a central wiring file. Three nouns carry the whole model: a plugin declares a manifest, the manifest names a set of contributions against a fixed set of axes, and a generated registry turns those declarations into static modules the runtime imports.

// definePlugin(name, version) returns a builder. No behavior runs here —
// each .with*() call only NAMES a contribution against a fixed axis.
import { definePlugin } from '@netscript/plugin';

export const workersManifest = definePlugin('@netscript/plugin-workers', '0.0.1-alpha.12')
  .withService({ name: 'workers-api', entrypoint: './services/src/main.ts', port: 8091 })
  .withBackgroundProcessor({ name: 'workers-worker', entrypoint: './bin/worker.ts', concurrency: 2 })
  .withDbSchemas([{ path: './database/workers.prisma', engine: 'postgres' }])
  .build();

Because the host only understands a fixed set of contribution shapes — a service, a background processor, a schema fragment, a stream topic, a config topic — but any number of plugins may contribute against them, capabilities compose without the host ever growing new code. Adding one is a regeneration step (netscript plugin add … then regenerate the registry), never an edit to someone else's boundary. And because a plugin's HTTP API and its background work are materialized as separate resources, a crash in a worker never takes down the API.

A single plugin splits into an HTTP API service resource and one or more isolated background-processor resources, each started by the AppHost as a separate Aspire resource with its own permissions.
"One plugin" is a packaging unit, not a runtime unit: at runtime it fans out into one API service and N isolated background resources.

The full walkthrough — core/plugin split, the contribution axes, and why the registry is generated rather than scanned — is in the plugin system . The auth plugin is the model at its richest: see the auth model .

Idea 3 — One runtime, many resources

The third idea is what turns a pile of packages into a system you can run. NetScript is Deno-native and distributed on JSR — you import from jsr:@netscript/* and use Web Platform APIs you already know. But a real backend is never one process, so the generated workspace ships an AppHost that brings every resource up together through .NET Aspire: your database, your cache (redis by default, or garnet / deno-kv via --cache-backend), each plugin's service and background processors, and a real dashboard — locally and on the way to deploy.

The AppHost graph: Postgres and Redis at the base; service, workers-api, sagas, triggers, auth-api, and streams resources above them; the Aspire dashboard observing all of them.
One command brings up the whole graph: data stores first, then every service and background resource, with the dashboard wired in for traces and logs.

The flow is two commands — cd aspire && aspire start brings up your database and Redis before any netscript db command touches the database — and you watch the result in the Aspire dashboard at :18888. Postgres is the recommended default; pass --db mysql, --db mssql, or --db sqlite at scaffold time to pick another engine (Postgres, MySQL, and SQL Server run as Aspire container resources, while SQLite is file-backed).

What comes up when you run the workspace (ports are range-allocated conventions; the dashboard is the authority)
NameTypeDescription
Aspire dashboard :18888 Every resource, trace, and log in one place — you don't stand up your own observability stack.
Your service :3000 The oRPC API the contract drives, served at /api/rpc/* with OpenAPI and health wired in.
Plugin services :8091–:8094 Each plugin's API runs as its own resource — workers :8091, sagas :8092, triggers :8093, auth :8094.
Data stores Postgres · Redis Brought up first, before migrations run; provisioned by the AppHost, not a hand-written compose file. Postgres is the default engine — swap it for mysql / mssql / sqlite via --db.

The whole runtime is opt-out, not lock-in: scaffold with netscript init my-app --no-aspire and the .NET footprint is gone, leaving the Deno workspace you own. How the AppHost reads manifests and materializes resources is covered in orchestration with Aspire and the runtime configuration hub .

How the three ideas connect

They are one idea seen from three angles. The contract defines what a capability exposes; a plugin packages that capability and declares how it contributes; the runtime materializes every contribution as an isolated resource and traces it end to end. Drift can't open up between layers because each layer derives from the one before it — the client from the contract, the registry from the manifests, the resource graph from the registry.