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.
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.
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 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).
| Name | Type | Description |
|---|---|---|
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.