Skip to main content
0.0.x

Define the public boundary once. NetScript carries it through the stack.

NetScript is a meta-framework for Deno. It composes Fresh, Hono, oRPC, and .NET Aspire into one workspace, where a database-backed shape can flow from a generated model schema into a versioned contract, then through the service to the rendered page — and the whole system runs as one observed resource graph.

Frameworks can supply the web layer, the service layer, and the orchestrator while leaving their boundaries to the application. Those boundaries are where request types diverge, where durable work is reduced to local retry loops, and where incident evidence splits across disconnected logs, resource state, and traces. NetScript gives an operation one versioned public contract—narrowed or extended from a generated model schema when one exists, authored directly when it does not—then uses it to type the handler and client and generate the OpenAPI description. What is derived from the contract cannot drift from it.

One definition, carried end to end

An optional DB-backed predecessor flows from a database model through db generate and @database/zod into a narrowed or extended versioned API schema. That schema and its oRPC route type the service handler, generated OpenAPI document, derived SDK client, page loader, and island. DB-less products can start at the versioned API schema.
DB-backed products normally narrow or extend generated model schemas before defining the public contract; DB-less products start at the versioned API schema. From the contract onward, the handler, OpenAPI document, SDK, and page share its types.

The same operation at four points in a DB-backed stack. Tab 0 is the optional generation and narrowing step; DB-less products begin at tab 1. The contract owns the public input and output shapes; the handler and page remain application code, but their boundary types come from it.

// From the workspace root:
// netscript db generate --db postgres

// contracts/versions/v1/users.schemas.ts
import { UserSchema as DatabaseUserSchema } from '@database/zod';
import { z } from 'zod';

export const UsersListItemSchemaV1 = DatabaseUserSchema
  .pick({ id: true, name: true })
  .extend({
    status: z.enum(['active', 'invited']),
  });
// contracts/versions/v1/users.contract.ts
import { z } from 'zod';
import { oc } from '@orpc/contract';
import { implement } from '@orpc/server';
import { UsersListItemSchemaV1 } from './users.schemas.ts'; // derived in tab 0, or hand-authored without a database

export const UsersListInputSchemaV1 = z.object({
  limit: z.number().int().positive().max(100).default(20),
});

// The contract binds method + input + output. No handler yet.
export const UsersContractV1 = {
  list: oc.route({ method: 'POST' })
    .input(UsersListInputSchemaV1)
    .output(z.object({ items: z.array(UsersListItemSchemaV1) })),
};

export const UsersV1 = implement(UsersContractV1);
// services/users/src/routers/v1.ts
import { UsersV1 as usersContract } from '@my-app/contracts';

// `input` is typed from the contract. The return value must
// satisfy the contract's output schema — or it does not compile.
export const UsersV1 = {
  list: usersContract.list.handler(async ({ input }) => {
    const items = await listUsers({ limit: input.limit });
    return { items };
  }),
};

// services/users/src/main.ts
// defineService() serves the router with OpenAPI, typed RPC,
// health endpoints, and request logging wired in.
import { defineService } from '@netscript/service';
import { router } from './router.ts';

await defineService(router, { name: 'users', version: '1.0.0' });
// apps/dashboard/lib/users.ts
import { createServiceClient } from '@netscript/sdk/client';
import { createQueryFactories } from '@netscript/sdk/query';
import { UsersContractV1 } from '@my-app/contracts';

export const usersName = 'users';
export const usersRouterName = 'users';
export const usersContract = UsersContractV1;
export const usersClient = createServiceClient<typeof usersContract>({
  contract: usersContract,
  serviceName: usersName,
  routerName: usersRouterName,
});
export const usersQueries = createQueryFactories({
  service: { contract: usersContract, client: usersClient },
}).service;

// apps/dashboard/routes/users/index.tsx
import { definePage } from '@netscript/fresh/builders';
import { z } from 'zod';
import { usersQueries } from '@app/lib/users.ts';
import { routes } from '@app/router.ts';
import UsersTable from './(_islands)/UsersTable.tsx';

export const usersPage = definePage()
  .withRoute(routes.users.$route)
  .withSearchParams(z.object({
    limit: z.coerce.number().int().positive().max(100).default(20),
  }))
  .withLayer('users', UsersTable, {
    loader: async (ctx) => {
      const input = { limit: ctx.search.limit };
      // Server-only: getCachedEntry uses the registered cache provider.
      const entry = await usersQueries.list.getCachedEntry(input);
      if (entry) return { users: entry.data, cachedAt: entry.cachedAt };
      return {
        users: await usersQueries.list.queryOptions(input).queryFn(),
        cachedAt: Date.now(),
      };
    },
  })
  .withLayout((slots) => <main class='ns-page'>{slots.users()}</main>)
  .build();

export const { default: page } = usersPage;
export { page as default };

Change the database model and regenerate in tab 0, or change the public schema in tab 1: incompatible handler code in tab 2 stops compiling, the OpenAPI document updates, and the loader in tab 3 sees the new types before anything reaches a browser. That propagation is the meta-framework: not a bigger library, but ownership of the seams between the libraries you already know. How the type flow works · how pages consume it.

What the framework carries for you

Pages arrive with their data

definePage composes route state, server loaders, and layered resources before an island hydrates, so the browser starts from useful HTML instead of rebuilding the page contract.

Interaction does not replace the server path

freshUiRegistryManifest and the design route give teams one component vocabulary; forms, optimistic updates, cache-first queries, and partials add progressively without making JavaScript the baseline.

The data layer you did not write

netscript db generate produces the Prisma client and Zod model schemas; createQueryFactories and createQueryCollection supply cache-aware caller views.

Authentication crosses one explicit seam

createAuthBackendRegistry selects one AuthBackendPort while the auth plugin carries its session contract, service, health surface, and audit spans into the workspace.

Installed capabilities join the whole system

definePlugin declares services, schemas, background processors, Aspire resources, and diagnostics in one manifest, so adding a capability also adds its operational shape.

Long work keeps its state and evidence

defineSaga gives stateful workflows typed state and outcomes; triggers and streams keep correlation, progress, and failure visible beyond the request that started them.

One operation leaves one trail

withSpan adds evidence to W3C trace context across service and background work; Aspire shows the resource graph, while createScalarDocs renders generated OpenAPI.

Agents inspect the framework before guessing

netscript agent mcp exposes bounded diagnostics, telemetry summaries, live operation schemas, and documentation search through the agent-facing MCP server.

For the UI path, go directly to server-validated forms, optimistic mutations, cache-first SDK bridge, or partial updates. For work beyond a request, compare worker jobs with durable streams. Follow a running system in the Aspire dashboard or expose its contract through the Scalar task.

Where to go