# Services & contracts

**Write the contract once; the handler, the typed client, and the OpenAPI surface are all derived from it — so an agent (or a teammate) never spends a turn keeping them in sync.**

A NetScript **service** is a typed HTTP runtime that *implements* an [`@orpc/contract`](https://rickylabs.github.io/netscript/explanation/contracts/) definition. You author the contract once (route + zod input/output), `implement()` it, bind `.handler()`s, and serve the resulting router on a Hono + oRPC runtime. The contract object is the single source of truth: the same object a typed client imports is the one the server implements, so caller and server **cannot drift**. The example `users` service answers on port **3001** with both an OpenAPI surface (`/api/v1/users/*`) and a typed oRPC endpoint (`/api/rpc/*`).

![A browser request flows into the service router, through middleware to the matched contract handler, optionally to the database, and back as a typed response.](https://rickylabs.github.io/netscript/netscript/assets/diagrams/request-lifecycle.svg)

*Request lifecycle: browser → service router → middleware → contract handler → database → typed response.*

> Use this when
>
> Reach for a service when you need a
>
> synchronous, request/response API
>
> with a versioned, type-locked contract shared between front end and back end — a CRUD surface, an internal RPC boundary, or a public REST/OpenAPI endpoint. For
>
> fire-and-forget
>
> work use a
>
> background job
>
> ; for
>
> message-driven, long-running
>
> orchestration use a
>
> durable saga
>
> ; for
>
> inbound HTTP that kicks off work
>
> use a
>
> trigger
>
> ; for
>
> signed-in identity and sessions
>
> compose the
>
> auth plugin
>
> .

## Why contract-first: the drift you never debug

Every hand-synced API dies the same way. A field gets renamed on the server; the client wrapper, the API docs, and the front-end types each get updated in separate steps — and one of them gets missed. The bug ships silently and surfaces later as a runtime `undefined`, and the fix costs a round of archaeology to find which of the three copies drifted.

A NetScript service removes the copies. The contract — plain `@orpc/contract` routes with zod input/output schemas, versioned in its own package — is the only declaration of the wire shape. The handler binds to it via `implement()`, the client imports it, and the OpenAPI spec is generated from it. Rename a field and the build fails in the handler *and* the caller before anything ships. In a production chat application built on NetScript, this is how the whole API surface works: `implement(ChannelContractV1)` on the service side, a typed dashboard client built off the same contract type on the other — a new contract route is automatically typed on the client, with no "keep the API docs in sync" step in between.

The same property is why contract-first services suit AI-agent codebases. Encore's [NestJS-alternatives article](https://encore.dev/articles/nestjs-alternatives) names the failure mode: TypeScript's backend ecosystem has many valid ways to structure validation, data access, and project layout, so coding agents pick a different combination on every prompt. Encore's answer is to fix the conventions in application code; NetScript locks the convention at the **contract layer** — one way to declare a route (contract + zod schemas + `implement()`), and everything downstream derived from it. An agent working in the codebase reads one declared shape and follows it, instead of inventing a new client wrapper per session.

## What it is

A service is the **Layer 3 preset** (`defineService`) over a three-layer package. Layer 1 is small primitives — health, error, RPC, OpenAPI, and Scalar docs handlers you can mount in any Hono app. Layer 2 is `createService()`, a fluent builder that materializes a mountable `ServiceApp` (via `build()`) or starts a Deno listener (via `serve()`). Layer 3 is `defineService()`, the curated one-call preset used by generated service entrypoints. Every layer takes the oRPC router as its input, and the same contract the router implements is the one a typed client imports — so a renamed field is a compile error on both sides. The full type story is in [Contracts](https://rickylabs.github.io/netscript/explanation/contracts/).

## Learn → / Do →

[Learn — Build a service  Track A 02: author a contract, stand up the users service on :3001, call it from a typed client.](https://rickylabs.github.io/netscript/netscript/tutorials/storefront/02-catalog-service/)

[Do — Expose OpenAPI & Scalar  Recipe: turn on the generated OpenAPI spec and the Scalar docs UI for an existing service.](https://rickylabs.github.io/netscript/netscript/services-sdk/how-to/expose-openapi-scalar/)

[Do — Graceful shutdown  Recipe: drain in-flight requests and run teardown hooks on SIGINT/SIGTERM before the process exits.](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/how-to/graceful-shutdown/)

## The contract is the source of truth

Before there is a service there is a contract. A contract is plain `@orpc/contract` routes whose inputs and outputs are zod schemas, collected into an object and passed to `implement()` from `@orpc/server`. `implement()` returns a `.handler()`-bindable object the service router consumes. The example workspace versions its contracts under `contracts/versions/v1/` and re-exports them as `@<project>/contracts` — so a contract bump is an explicit, reviewable version directory, never an accidental break.

```ts
// contracts/versions/v1/users.contract.ts
import { z } from 'zod';
import { oc } from '@orpc/contract';
import { implement } from '@orpc/server';

export const UsersContractV1 = {
  health: oc.route({ method: 'GET' }).input(z.object({}).optional()).output(UsersHealthSchemaV1),
  list: oc.route({ method: 'POST' }).input(UsersListInputSchemaV1).output(UsersListResponseSchemaV1),
  updateStatus: oc.route({ method: 'POST' }).input(UsersUpdateStatusInputSchemaV1).output(UsersUpdateStatusResponseSchemaV1),
};

// `implement()` produces the .handler()-bindable object the service router consumes.
export const UsersV1 = implement(UsersContractV1);
```

The chain end-to-end — contract → `implement()` → `.handler()` → typed client → query → island — is laid out in [Contracts](https://rickylabs.github.io/netscript/explanation/contracts/). The key property: the client imports the *same* contract object, so a renamed field or a changed output shape is a **compile error in both the handler and the caller** before it can ship.

## Headline API: two ways to construct a service

NetScript ships **two** service-construction APIs, and the example project uses both. Workspace services use the one-call `defineService(router, options)` — the right default for the 80% case. Plugin API services (workers, sagas, auth) use the fluent `createService(router, options).with*().serve()` builder when they need to add CORS, OpenAPI, a database client, auth middleware, or custom context step by step. Both stand up the same Hono + oRPC runtime; `defineService` is a curated preset over the same builder.

```ts
// services/users/src/main.ts
import { defineService } from '@netscript/service';
import { router } from './router.ts';

// One call wires CORS, request logging, OpenAPI, RPC, and health endpoints.
await defineService(router, {
  name: 'users',
  version: '1.0.0',
  port: parseInt(Deno.env.get('PORT') || '3001'),
  openapi: { title: 'Users API', description: 'users service' },
  debug: true,
});
```

```ts
// plugins/workers/services/src/main.ts — step-by-step composition
import { createService } from '@netscript/service';
import { router } from './router.ts';

await createService(router, { name: 'workers', version: '1.0.0', port: 8091 })
  .withCors()
  .withLogger()
  .withOpenAPI({ title: 'Workers API' })
  .withDocs()
  .withDatabase(dbClient)
  .withContext(() => ({ workers: runtime }))
  .withRPC({ traceContext: true })
  .withHealth()
  .withServiceInfo()
  .onStartup(async () => {/* seed, warm caches */})
  .serve();
```

Aspire injects `PORT` at runtime, so the entrypoint reads it from the environment; the typed source of truth is your `netscript.config.ts` `services.<name>.port` field, which the scaffold wires as the fallback default — set the port there rather than editing this line.

### `defineService(router, options)` — the preset options

`DefineServiceOptions` extends the base `ServiceConfig` (`name`, `version`, `port`) and adds the preset-only keys below. These are the **complete** option keys confirmed against the package surface — nothing is omitted.

**DefineServiceOptions (extends ServiceConfig)**

| Name | Type | Description |
| --- | --- | --- |
| `name` | `string (required)` | Service name used for logging, telemetry, and health-check labels. |
| `version` | `string?` | Service version (e.g. '1.0.0'); surfaced on /health and the OpenAPI spec. |
| `port` | `number?` | Default listener port if serve() is not passed an explicit port. The generated entrypoint reads Deno.env.get('PORT') \|\| '3001'. |
| `db` | `DbContext?` | Database context injected as context.db. Accepts a single Prisma client (with $queryRaw) or a multi-db record like { netscript, mdb, prosco, prev }; the first value exposing $queryRaw is auto-wired as the /health and /health/ready probe client. |
| `openapi` | `{ title?; description? }?` | Turns on the generated OpenAPI spec endpoint and the Scalar docs UI with this title/description. |
| `debug` | `boolean?` | Enables verbose oRPC logging. Defaults to the NETSCRIPT_DEBUG env var. |
| `auth` | `{ authn: AuthnOptions; authz?: AuthzOptions }?` | Installs the authentication (and optional authorization) gate on guarded paths — the preset form of .withAuthn()/.withAuthz(). |
| `tls` | `ServiceTlsOptions?` | Opt-in TLS: { cert, key } as PEM strings. When set, the listener serves HTTPS and negotiates HTTP/2 via ALPN automatically. Forwarded to serve() as .serve({ tls }). See TLS & HTTP/2 below. |

> Handlers bind to the contract
>
> A router aggregates versioned handlers —
>
> export const router = { v1: { users: { ...UsersV1, health } } }
>
> — and each handler is bound from the implemented contract:
>
> v1.users.list.handler(async ({ input }) => { /* return records */ })
>
> . In the scaffold's
>
> Step 5
>
> the
>
> users
>
> handlers return seeded
>
> in-memory records
>
> (no database yet) — the front-end ↔ contract proof. Wiring Prisma comes later; see
>
> Database
>
> .

## Endpoints & ports

A `defineService` runtime exposes its OpenAPI routes, a typed oRPC endpoint mounted under `/api/rpc/*`, and a health check. The example `users` service is reachable once `aspire start` is up (Aspire provisions Postgres/Redis first) or when you run it directly with `deno task --cwd services/users dev`.

**Users service surface (port 3001)**

| Name | Type | Description |
| --- | --- | --- |
| `/api/v1/users/*` | `HTTP/OpenAPI` | REST surface generated from the contract (list, updateStatus, health). |
| `/api/rpc/*` | `oRPC` | Typed RPC endpoint a generated client calls — same contract object, no drift. This is the served default path (not /rpc). |
| `/health` | `HTTP` | Liveness/readiness check; anonymous by default (excluded from authn). |
| `:3001` | `port` | Default service port; read from Deno.env.get('PORT') \|\| '3001'. |

> Aspire first, then hit the endpoint
>
> Services that touch the database need orchestration up first:
>
> cd aspire && aspire start
>
> brings up Postgres and Redis (dashboard at
>
> https://localhost:18888
>
> )
>
> before
>
> any
>
> netscript db
>
> command. The seeded-records example service runs without the DB, but the real workflow expects Aspire running. See
>
> Aspire
>
> .

## OpenAPI spec & Scalar docs

Passing `openapi` to `defineService` (or calling `.withOpenAPI().withDocs()` on the builder) turns on a REST surface generated from the same contract, a machine-readable OpenAPI JSON spec, and the **Scalar** interactive docs UI. Under the hood three Layer-1 primitives do the work, and you can mount them directly in any host Hono app when you need finer control: `createOpenAPISpec` serves the spec JSON, `createScalarDocs` serves the Scalar HTML page, and `createOpenAPIHandler` serves the REST routes themselves (it adds the `ZodSmartCoercionPlugin` so query-string values coerce to their schema types automatically).

**OpenAPI / Scalar primitives (@netscript/service)**

| Name | Type | Description |
| --- | --- | --- |
| `createOpenAPISpec(router, config)` | `→ ServiceHandler` | Serves the OpenAPI JSON spec. config is OpenAPIConfig (see below). Mount at e.g. /api/openapi.json. |
| `createScalarDocs(options)` | `→ ServiceHandler` | Serves the Scalar docs UI HTML that loads the spec. options is ScalarDocsOptions (specUrl, title?, theme?). |
| `createScalarJs()` | `→ ServiceHandler` | Serves the bundled Scalar JS so the docs UI works offline without CDN access. |
| `createOpenAPIHandler(router, config?)` | `→ FetchHandler` | The OpenAPI REST request handler (with ZodSmartCoercionPlugin). config is RPCHandlerConfig; mount under /api/*. |

**OpenAPIConfig (createOpenAPISpec)**

| Name | Type | Description |
| --- | --- | --- |
| `title` | `string (required)` | API title shown in the spec and docs. |
| `version` | `string (required)` | API version, e.g. '1.0.0'. |
| `description` | `string?` | Longer API description. |
| `servers` | `Array<{ url; description? }>?` | Server URLs advertised in the spec (e.g. staging vs. production base URLs). |

**ScalarDocsOptions (createScalarDocs)**

| Name | Type | Description |
| --- | --- | --- |
| `specUrl` | `string (required)` | URL the docs UI fetches the OpenAPI spec from, e.g. '/api/openapi.json'. |
| `title` | `string?` | Docs page title. |
| `theme` | `'default' \| 'kepler' \| 'moon' \| 'purple' \| 'saturn'` | Scalar UI theme. |

```ts
// services/users/src/main.ts
import { defineService } from '@netscript/service';
import { router } from './router.ts';

// Turns on the OpenAPI spec + Scalar docs UI in one option.
await defineService(router, {
  name: 'users',
  version: '1.0.0',
  port: 3001,
  openapi: {
    title: 'Users API',
    description: 'User management service',
  },
});
```

```ts
// host/openapi-routes.ts — wire the primitives onto an existing Hono app
import {
  createOpenAPISpec,
  createScalarDocs,
  createScalarJs,
} from '@netscript/service';
import { router } from './router.ts';

app.get('/api/openapi.json', createOpenAPISpec(router, {
  title: 'Users API',
  version: '1.0.0',
  description: 'User management service',
}));

app.get('/api/docs', createScalarDocs({
  specUrl: '/api/openapi.json',
  title: 'Users API',
  theme: 'kepler',
}));

// Serve Scalar offline (no CDN dependency)
app.get('/api/docs/scalar.js', createScalarJs());
```

## Health, readiness & liveness

The preset wires `/health` automatically; the underlying primitives let you compose probes by hand or mount them in a host app. `createHealthHandler` runs every registered `HealthCheck` in parallel and returns an aggregate `HealthResponse` (`status` ∈ `healthy | degraded | unhealthy`). `createReadinessHandler` takes an array of async boolean checks and is the one to point an orchestrator's *readiness* probe at — it fails until dependencies (DB, caches) are reachable. `createLivenessHandler` is a bare "is the process up" probe that returns 200 with no dependency checks, for an orchestrator's *liveness* probe. The `healthChecks` namespace ships pre-built checks for the common dependencies so you don't hand-roll them.

**Health primitives & checks (@netscript/service)**

| Name | Type | Description |
| --- | --- | --- |
| `createHealthHandler(options?)` | `→ ServiceHandler` | Runs all checks in parallel, returns an aggregate HealthResponse. options: HealthHandlerOptions { checks?, version?, includeDetails? (default true) }. |
| `createReadinessHandler(checks)` | `→ ServiceHandler` | Readiness probe: checks is Array<() => Promise>; reports not-ready until every check resolves true. Mount at /health/ready. |
| `createLivenessHandler()` | `→ ServiceHandler` | Liveness probe: 200 OK while the process runs, no dependency checks. Mount at /health/live. |
| `healthChecks.database(db)` | `→ HealthCheck` | Pre-built DB probe; runs a $queryRaw `SELECT 1`-style ping against the Prisma client. |
| `healthChecks.kv()` | `→ HealthCheck` | Pre-built check for the KV store. |
| `healthChecks.service(name, baseUrl)` | `→ HealthCheck` | Probes another service's health endpoint by name + base URL. |
| `healthChecks.custom(name, fn)` | `→ HealthCheck` | Wraps your own async () => Promise as a named check. |

```ts
// host/health-routes.ts — readiness + liveness + aggregate health
import {
  createHealthHandler,
  createLivenessHandler,
  createReadinessHandler,
  healthChecks,
} from '@netscript/service';
import { db } from '@database';

// Aggregate health (parallel checks + details)
app.get('/health', createHealthHandler({
  version: '1.0.0',
  checks: [healthChecks.database(db)],
}));

// Liveness: is the process up?
app.get('/health/live', createLivenessHandler());

// Readiness: are dependencies reachable?
app.get('/health/ready', createReadinessHandler([
  async () => { await db.$queryRaw`SELECT 1`; return true; },
]));
```

> Liveness vs. readiness
>
> Point an orchestrator's
>
> liveness
>
> probe at
>
> /health/live
>
> (restart the container only when the process is wedged) and its
>
> readiness
>
> probe at
>
> /health/ready
>
> (stop routing traffic until the DB and caches answer). Wiring both to the same dependency-checking endpoint is the classic footgun: a transient DB blip then triggers a
>
> restart
>
> instead of a brief traffic pause.

## Graceful shutdown

`serve()` installs SIGINT/SIGTERM (or SIGBREAK on Windows) handlers and drains in-flight requests before exiting. Register teardown work with `.onShutdown(hook)` on the builder — a `ShutdownHook` receives a `ShutdownContext` (`reason`, optional `signal`) and runs during the drain. The drain is bounded by `drainTimeoutMs` (default `30_000`); when it elapses the service stops anyway and the resulting `ShutdownReport` records `timedOut: true` plus a per-hook `ShutdownHookOutcome` for each registered hook. Calling `running.stop()` triggers the same drain manually (reason `'manual'`).

**Shutdown types (@netscript/service)**

| Name | Type | Description |
| --- | --- | --- |
| `ShutdownHook` | `(context: ShutdownContext) => Promise \| void` | Async teardown callback registered via .onShutdown(). Run in registration order during the drain. |
| `ShutdownContext` | `{ reason: ShutdownReason; signal?: Deno.Signal }` | Passed to each hook. signal is set only when reason is 'signal'. |
| `ShutdownReason` | `'signal' \| 'manual' \| 'startup-failure'` | Why the drain started: an OS signal, a manual stop() call, or a failed startup hook. |
| `ShutdownHookOutcome` | `{ ok: boolean; error?: string }` | Per-hook result; error holds a normalized message when a hook throws or rejects. |
| `ShutdownReport` | `{ reason; timedOut: boolean; hooks: readonly ShutdownHookOutcome[] }` | Final result of a completed shutdown — the reason, whether the timeout elapsed, and every hook outcome in execution order. |

**ServeOptions (serve()) — shutdown-relevant keys**

| Name | Type | Description |
| --- | --- | --- |
| `port` | `number?` | Preferred listener port; use 0 for an ephemeral port. |
| `signal` | `AbortSignal?` | External signal that stops the listener when aborted (drives the drain). |
| `drainTimeoutMs` | `number?` | Max time to wait for in-flight requests and shutdown hooks before forcing exit. Defaults to 30_000. |
| `handleSignals` | `boolean?` | Install SIGINT/SIGTERM (or SIGBREAK) handlers. Defaults to true. |
| `tls` | `ServiceTlsOptions?` | Opt-in TLS ({ cert, key } PEM strings). Present → HTTPS + HTTP/2 via ALPN; absent → plain HTTP/1.1. See TLS & HTTP/2 below. |

```ts
// services/users/src/main.ts — drain + teardown on signal or manual stop
import { createService } from '@netscript/service';
import { router } from './router.ts';
import { db } from '@database';

const running = await createService(router, { name: 'users', version: '1.0.0' })
  .withRPC()
  .withHealth()
  .onShutdown(async ({ reason, signal }) => {
    // reason: 'signal' | 'manual' | 'startup-failure'
    audit.record({ event: 'shutdown', reason, signal });
    await db.$disconnect();
  })
  .serve({
    port: 3001,
    drainTimeoutMs: 10_000, // wait up to 10s for in-flight work
    handleSignals: true,    // SIGINT/SIGTERM/SIGBREAK
  });

// Later, in tests or a supervisor: trigger the same drain manually.
await running.stop();
```

## TLS & HTTP/2 (opt-in)

By default a service listens over plain HTTP. Pass a `tls` option and the *same* listener serves HTTPS and negotiates **HTTP/2 automatically via ALPN** — HTTP/1.1 stays available as a fallback, and there is no separate `http2` flag. TLS is purely additive: a service that omits `tls` is unchanged.

`tls` is a `ServiceTlsOptions` object whose `cert` and `key` are **PEM-encoded strings** — the certificate chain and the private key as *contents*, not file paths. It is accepted by both `defineService(router, { …, tls })` and the fluent builder's `.serve({ tls })`.

```ts
// services/users/src/main.ts — serve HTTPS + HTTP/2 with an inline cert/key
import { defineService } from '@netscript/service';
import { router } from './router.ts';

await defineService(router, {
  name: 'users',
  port: 3000,
  tls: {
    cert: await Deno.readTextFile('cert.pem'), // PEM contents
    key: await Deno.readTextFile('key.pem'),   // PEM contents
  },
});
// Fluent equivalent: createService(router, { name }).withHealth().serve({ tls: { cert, key } })
```

If you omit `tls`, the listener falls back to environment configuration: set **both** `NETSCRIPT_TLS_CERT_FILE` and `NETSCRIPT_TLS_KEY_FILE` and the service serves HTTPS from those files. Unlike the inline `cert`/`key` (PEM contents), these env vars are **file paths** — and both are required; one alone is ignored.

**ServiceTlsOptions (@netscript/service) + env fallback**

| Name | Type | Description |
| --- | --- | --- |
| `cert` | `string (required)` | PEM-encoded certificate chain — the contents, not a path. |
| `key` | `string (required)` | PEM-encoded private key — the contents, not a path. |
| `NETSCRIPT_TLS_CERT_FILE` | `env (path)` | Fallback when tls is omitted: file path to the PEM certificate chain. Both env vars must be set together. |
| `NETSCRIPT_TLS_KEY_FILE` | `env (path)` | Fallback when tls is omitted: file path to the PEM private key. Both env vars must be set together. |

## Service-layer authn / authz middleware

The service builder ships a **provider-agnostic** authentication and authorization seam in `@netscript/service/auth`. It is a thin Hono-middleware layer over the request pipeline — deliberately **distinct from the [auth plugin](https://rickylabs.github.io/netscript/identity-access/auth/)**, which composes a sign-in/session backend (kv-oauth, WorkOS, better-auth). Use this seam when a service needs to gate its own routes — verify a credential, trust an upstream identity header, or check a scope — without taking on an interactive identity provider.

Two stages wrap the request: `.withAuthn()` resolves a `Principal` (authentication) via an `AuthenticatorPort`, and `.withAuthz()` makes an `AuthzDecision` from that principal (authorization) via an `AuthorizerPort`. By default the `/api` surface is protected and `/health` is anonymous — both configurable through `AuthnOptions.protect` / `AuthnOptions.allowAnonymous`. The preset form is `defineService(router, { auth: { authn, authz } })`.

**@netscript/service/auth surface**

| Name | Type | Description |
| --- | --- | --- |
| `createStaticCredentialAuthenticator` | `authenticator` | Matches a configured static credential (e.g. API key / shared secret) → Principal. |
| `createTrustedHeaderAuthenticator` | `authenticator` | Trusts an identity asserted by an upstream proxy header (e.g. behind a gateway). |
| `createScopeAuthorizer` | `authorizer` | Allows/denies a request by checking the Principal's scopes against required scopes. |
| `.withAuthn({ authenticator, protect?, allowAnonymous? })` | `builder method` | Installs the authentication gate (AuthnOptions). protect defaults to ['/api']; allowAnonymous defaults to ['/health']. |
| `.withAuthz({ authorizer, denyByDefault? })` | `builder method` | Installs the authorization gate (AuthzOptions) from the Principal. denyByDefault fails closed and defaults to true. |

```ts
// Gate a service with a static credential + scope check
import { createService } from '@netscript/service';
import {
  createStaticCredentialAuthenticator,
  createScopeAuthorizer,
} from '@netscript/service/auth';
import { router } from './router.ts';

const authenticator = createStaticCredentialAuthenticator({
  credentials: {
    [Deno.env.get('SERVICE_API_KEY') ?? '']: {
      subject: 'service:ci',
      scopes: ['users:write'],
    },
  },
});
const authorizer = createScopeAuthorizer({
  rules: [{ match: () => true, requireScopes: ['users:write'] }],
  denyByDefault: true,
});

await createService(router, { name: 'users', version: '1.0.0', port: 3001 })
  .withRPC()
  .withAuthn({ authenticator })
  .withAuthz({ authorizer })
  .withHealth() // /health stays anonymous
  .serve();
```

```ts
// Trust identity asserted by an upstream proxy/gateway
import { createTrustedHeaderAuthenticator } from '@netscript/service/auth';

// The Principal is assembled internally from these header NAMES — point at the
// headers your gateway sets; you do not map values yourself.
const authenticator = createTrustedHeaderAuthenticator({
  subjectHeader: 'x-forwarded-user',
  scopesHeader: 'x-forwarded-scopes', // optional; space/comma-separated
});

// then: .withAuthn({ authenticator })
```

> Service-auth seam vs. the auth plugin
>
> This middleware is for
>
> machine-to-machine and gateway-fronted
>
> gating: static credentials, trusted upstream headers, scope checks. It does
>
> not
>
> do interactive sign-in, OAuth callbacks, or session cookies. For
>
> signed-in human users
>
> (sign-in / callback / session / me), compose the
>
> `auth` plugin
>
> backend instead — the two layers are complementary and can run together.

## Production notes

> Footguns before you ship
>
> - **Don't wire liveness to dependency checks.** `/health/live` must stay dependency-free; only `/health/ready` should fail on an unreachable DB. Crossing them turns a transient DB blip into a container restart loop.
> - **Set `drainTimeoutMs` below your platform's kill grace.** The drain defaults to `30_000`; if your orchestrator sends SIGKILL sooner, in-flight requests are cut off — pick a timeout under the platform's termination grace period.
> - **The served RPC path is `/api/rpc/*`, not `/rpc`.** Point generated clients at the same path the runtime mounts.
> - **`denyByDefault` is `true`.** With `.withAuthz()` installed, any request that reaches no matching rule is rejected — add an explicit allow rule for public-but-authenticated routes rather than relaxing the default.
> - **Aspire first for DB-backed services.** `aspire start` must bring up Postgres/Redis before any `netscript db` command or the readiness probe will fail.

## Reference

This hub is intentionally thin — the full generated API lives in the reference.

[service](https://rickylabs.github.io/netscript/netscript/reference/service/)

[Look up — @netscript/service  The full generated API: defineService, createService, the fluent builder, the OpenAPI/Scalar and health primitives, the shutdown types, and the /auth middleware seam.](https://rickylabs.github.io/netscript/netscript/reference/service/)

[Look up — @netscript/contracts  Contract primitives plus the /crud, /query, and /transform helpers that generate CRUD contracts, paginated queries, and response transformers.](https://rickylabs.github.io/netscript/netscript/reference/contracts/)

[Understand — Contracts  The oRPC contract → implement → handler → typed client → query → island type flow, explained.](https://rickylabs.github.io/netscript/netscript/explanation/contracts/)

[Learn — Build a service  Guided tutorial: contract → users service on :3001 → typed client → island, built from scratch.](https://rickylabs.github.io/netscript/netscript/tutorials/storefront/02-catalog-service/)

[Capabilities](https://rickylabs.github.io/netscript/netscript/capabilities/) [Background jobs](https://rickylabs.github.io/netscript/netscript/background-processing/workers/)
