# Add auth and a session

You have a running workspace, but anyone can hit it — and for the app this track is building, that is not a cosmetic gap. A team workspace exists to hold things the whole internet must not read: member lists, project records, the operational context a team accumulates while something is on fire. Before any of that data lands (chapter 3), the app needs to answer one question reliably: *who is this?* This chapter gives it that identity layer: you add the official `auth` plugin, choose an authentication **backend**, run its database migration, and verify a live **session** through the `auth-api` service on `:8094`. The lesson underneath the steps: **auth in NetScript is a pluggable backend plus a session** — you pick the backend with one environment variable, and the contract is identical no matter which one you pick. Swapping providers later is a config change, not a rewrite you carry through every route.

1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/01-scaffold/)
2. [2 · Auth](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/02-auth/)
3. [3 · Workspace data](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/03-workspace-data/)
4. [4 · Provision job](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/04-provision-job/)
5. [5 · Route authz](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/05-route-authz/)
6. [6 · Deploy](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/06-deploy/)

## What you will build

A working sign-in surface for `my-workspace/`: the `auth` plugin installed, the interactive `kv-oauth` backend selected, the `auth.prisma` migration applied, and the `auth-api` service answering on `:8094` (note: this tutorial uses port 8094 for the auth-api; in unpinned scaffolds, each project is allocated its own randomized high-range ports). By the end you can hit `GET /api/v1/auth/session` and `GET /api/v1/auth/me` and watch the service correctly report "no session yet" before login — proof the backend is composed and the session endpoints are wired.

## Before you begin

You need the workspace from [chapter 1](https://rickylabs.github.io/netscript/tutorials/workspace/01-scaffold/) with **Aspire running** — the auth plugin contributes a service and a Prisma schema that Aspire and `netscript db` reach through the running graph. Confirm the base is up:

```sh
# In my-workspace/, with `aspire start` up in another terminal
curl http://localhost:3001/health   # the workspace service from chapter 1
```

> Aspire is the control plane — start it first
>
> The
>
> auth-api
>
> service and its Postgres/KV dependencies are resources in the Aspire graph. Bring orchestration up
>
> before
>
> any
>
> netscript db
>
> command or auth endpoint call: from the project root,
>
> cd aspire && aspire start
>
> (dashboard at
>
> https://localhost:18888
>
> ). DB commands need Aspire running first.

## Step 1 — Add the `auth` plugin

The `auth` plugin is a first-class official plugin, installed the same way as `workers`, `sagas`, and `triggers`. From the workspace root:

```sh
netscript plugin install @netscript/plugin-auth --port 8094
```

This scaffolds the unified `@netscript/plugin-auth` plugin into `plugins/`, registers it, and contributes three things to your workspace: a Prisma schema (`auth.prisma`), a service entry that becomes the `auth-api` service, and the `/api/v1/auth/*` routes.

`--port 8094` is what makes the rest of this chapter's `curl`s work. Without it the installer picks a port for you and writes it as the resource's `HostPort`, so `auth-api` would answer on some number this page cannot print. Pinning has a cost — a pinned host port is a machine-global reservation, so `aspire start --isolated` can no longer randomise it away and a second workspace pinning 8094 will collide. That is an acceptable trade for a tutorial; for real work, omit `--port` and read the endpoint off the dashboard. Confirm it landed:

```sh
netscript plugin list
```

> Single Active Backend Design Boundary
>
> The
>
> @netscript/plugin-auth
>
> plugin composes a single active backend adapter at runtime (configured via
>
> NETSCRIPT_AUTH_BACKEND
>
> ). By design, the NetScript runtime isolates identity provider environments to keep session verification simple and predictable, so running multiple active backends simultaneously or linking accounts across them is not supported. For this tutorial, we will configure a single provider, but in production, any provider swapping is performed at the environment level rather than through runtime routing.

> Alpha package pins
>
> The scaffold emits exact alpha specifiers such as
>
> jsr:@netscript/plugin-auth-core@0.0.6
>
> . Add auth through
>
> netscript plugin install @netscript/plugin-auth
>
> , which wires the workspace correctly for the aligned alpha train.

## Step 2 — Choose a backend with `NETSCRIPT_AUTH_BACKEND`

The active backend is selected by the `NETSCRIPT_AUTH_BACKEND` environment variable (or the `auth.backend` appsettings key). Three backends are valid; the default is **`kv-oauth`**, the only one that drives an interactive sign-in.

**Auth backends — capability matrix (NETSCRIPT_AUTH_BACKEND)**

| Name | Type | Description |
| --- | --- | --- |
| `kv-oauth` | `interactive (default)` | Full OAuth/OIDC redirect flow. Real signin + callback, KV-backed sessions with refresh-on-read, signout. The only backend that implements InteractiveFlowPort. Package @netscript/auth-kv-oauth. |
| `workos` | `non-interactive` | WorkOS AuthKit sealed wos-session cookie. Validates an existing session; signin/callback return AUTH_PROVIDER_ERROR. Package @netscript/auth-workos. |
| `better-auth` | `non-interactive` | better-auth over Prisma. Validates an existing session; signin/callback return AUTH_PROVIDER_ERROR. Package @netscript/auth-better-auth. |

A team workspace needs users to *log in*, so this track uses the interactive default. Make the choice explicit in your environment:

```sh
export NETSCRIPT_AUTH_BACKEND=kv-oauth
```

> Only kv-oauth is interactive — choose accordingly
>
> The
>
> signin
>
> and
>
> callback
>
> endpoints require a backend that implements the optional
>
> InteractiveFlowPort
>
> .
>
> Only `kv-oauth` does.
>
> On
>
> workos
>
> and
>
> better-auth
>
> ,
>
> POST /api/v1/auth/signin
>
> or
>
> POST /api/v1/auth/callback
>
> returns a typed
>
> AUTH_PROVIDER_ERROR
>
> (502) — those backends are for environments where sign-in already happened elsewhere and you only need NetScript to
>
> validate
>
> the session (
>
> session
>
> /
>
> me
>
> ). To drive the login redirect from NetScript, stay on
>
> kv-oauth
>
> .

## Step 3 — Run the auth database migration

The `auth` plugin contributes a package-provided **`auth.prisma`** schema that aggregates into your primary Postgres at `db generate`. With Aspire running, run the standard database loop from the workspace root:

```sh
netscript db init --name init    # first time only — create the migration
netscript db generate            # generate the Prisma client + Zod schemas
netscript db seed                # optional seed data
netscript db status              # confirm the migration is applied
```

> Which backends actually use these tables
>
> auth.prisma
>
> defines four better-auth-shaped models —
>
> AuthUser
>
> →
>
> auth_users
>
> ,
>
> AuthSession
>
> →
>
> auth_sessions
>
> ,
>
> AuthAccount
>
> →
>
> auth_accounts
>
> ,
>
> AuthVerification
>
> →
>
> auth_verifications
>
> . The migration runs regardless, but storage differs by backend:
>
> kv-oauth
>
> keeps sessions in Deno KV (not these tables),
>
> workos
>
> is effectively stateless (sealed cookie), and
>
> better-auth
>
> is the one that reads/writes these tables through Prisma.

## Step 4 — Configure the kv-oauth backend

The `kv-oauth` backend needs a real OAuth/OIDC provider (a client id, secret, and redirect URI) plus a key for the session token at rest. Set these in your environment before starting the service:

```sh
# Selects the interactive backend
export NETSCRIPT_AUTH_BACKEND=kv-oauth

# Provider credentials (e.g. a Google or GitHub OAuth app)
export NETSCRIPT_AUTH_CLIENT_ID=your-client-id
export NETSCRIPT_AUTH_CLIENT_SECRET=your-client-secret
export NETSCRIPT_AUTH_REDIRECT_URI=http://localhost:8094/api/v1/auth/callback

# Required for kv-oauth: missing key material is a startup error
export NETSCRIPT_AUTH_KV_OAUTH_KEY=<base64url-encoded-32-byte-secret>
```

Note there is no `PORT` here. Under `aspire start`, Aspire injects `PORT` into the service — that is the port the process binds inside the graph, and it is not the `:8094` you browse to. `:8094` is the **host** port, pinned by the `--port 8094` you passed at install; Aspire proxies it to whatever `PORT` it handed the process.

> Provider presets fill the OIDC endpoints for you
>
> You rarely hand-type issuer/authorization/token/userinfo URLs. The
>
> kv-oauth
>
> package ships provider presets —
>
> github
>
> ,
>
> google
>
> ,
>
> gitlab
>
> ,
>
> discord
>
> ,
>
> slack
>
> ,
>
> spotify
>
> ,
>
> facebook
>
> ,
>
> twitter
>
> , plus tenant-based
>
> auth0
>
> ,
>
> okta
>
> ,
>
> awsCognito
>
> ,
>
> azureAd
>
> ,
>
> logto
>
> ,
>
> clerk
>
> — that encode the correct endpoints. You pass one to
>
> createKvOAuthBackend
>
> in Step 5.

## Step 5 — Compose the backend in code

When you wire the backend yourself — for a custom service entry or a test — the interactive `kv-oauth` backend is one `await` call. You pass a provider preset from `providers.*`, and you get back something that satisfies the `AuthBackendPort` seam that `@netscript/plugin-auth-core` defines (and, because it is `kv-oauth`, the optional `InteractiveFlowPort` too):

```ts
// services/auth/src/backend.ts
import { createKvOAuthBackend, getRequiredEnv, providers } from '@netscript/auth-kv-oauth';

// providers.google(...) is a preset that fills the OIDC endpoints for you.
export const backend = await createKvOAuthBackend({
  provider: providers.google({
    clientId: getRequiredEnv('NETSCRIPT_AUTH_CLIENT_ID'),
    clientSecret: getRequiredEnv('NETSCRIPT_AUTH_CLIENT_SECRET'),
    redirectUri: getRequiredEnv('NETSCRIPT_AUTH_REDIRECT_URI'),
  }),
});

// backend implements AuthBackendPort AND the optional InteractiveFlowPort,
// so the auth-api signin + callback endpoints are live on this backend.
console.log(backend.name); // 'kv-oauth'
```

```ts
// Same factory, but define the OIDC provider yourself when your IdP is
// not one of the built-in presets.
import { createKvOAuthBackend, defineOAuthProvider, getRequiredEnv } from '@netscript/auth-kv-oauth';

const provider = defineOAuthProvider({
  id: 'my-idp',
  clientId: getRequiredEnv('NETSCRIPT_AUTH_CLIENT_ID'),
  clientSecret: getRequiredEnv('NETSCRIPT_AUTH_CLIENT_SECRET'),
  authorizationEndpoint: getRequiredEnv('NETSCRIPT_AUTH_AUTHORIZATION_ENDPOINT'),
  tokenEndpoint: getRequiredEnv('NETSCRIPT_AUTH_TOKEN_ENDPOINT'),
  userInfoEndpoint: getRequiredEnv('NETSCRIPT_AUTH_USERINFO_ENDPOINT'),
  redirectUri: getRequiredEnv('NETSCRIPT_AUTH_REDIRECT_URI'),
  scopes: ['openid', 'profile', 'email'],
});

export const backend = await createKvOAuthBackend({ provider });
```

The core seam is small and contract-only. `@netscript/plugin-auth-core` defines the `AuthBackendPort` and the registry helpers that resolve one backend from many — these are the types the auth plugin composes against, confirmed on the package's public surface:

**@netscript/plugin-auth-core — the seam the backend satisfies**

| Name | Type | Description |
| --- | --- | --- |
| `AuthBackendPort` | `type` | The contract every backend implements — the auth-api surface is identical across all three backends because they all satisfy this port. |
| `createAuthBackendRegistry / resolveBackend` | `function` | Build a registry of named backends and resolve the single active one (DEFAULT_AUTH_BACKEND_NAME is the fallback). |
| `AuthSession` | `type` | The normalized session the store persists — id, subject, state, scopes, claims, issuedAt / expiresAt. |
| `createHmacSessionTokenCrypto` | `function` | HMAC-signs the opaque session token so the cookie value cannot be forged. |
| `authContractV1` | `contract` | The five-route auth contract: signin, signout, callback, session, me. |

## Step 6 — Verify a session

With Aspire running, the `auth-api` service is reachable on its pinned host port **`:8094`** and mounts the five auth endpoints under `/api/v1/auth/*`. On a fresh, unauthenticated request, `session` reports no active session — which proves the endpoint is wired even before you complete a login:

```sh
# Service is alive
curl http://localhost:8094/health/ready

# No session yet — confirms the endpoint is mounted and reachable
curl http://localhost:8094/api/v1/auth/session

# Identity endpoint: { "authenticated": false } (HTTP 200) before login
curl http://localhost:8094/api/v1/auth/me
```

To exercise the full interactive flow on `kv-oauth`, drive the redirect from a browser: open `POST /api/v1/auth/signin` (the service issues the provider redirect), authenticate with your provider, let it call back to `/api/v1/auth/callback`, then re-check the session with the cookie the flow set:

```sh
# After completing the browser sign-in, the __Host-ns_session cookie is set.
curl -b cookies.txt http://localhost:8094/api/v1/auth/session
curl -b cookies.txt http://localhost:8094/api/v1/auth/me
```

A successful `GET /api/v1/auth/me` after sign-in returns `{ authenticated: true, user, session }`. That round trip is the proof the backend is composed, the migration is applied, and the provider credentials are correct.

- [ ] `netscript plugin list` shows the `auth` plugin.
- [ ] `netscript db status` reports the `auth.prisma` migration applied.
- [ ] `NETSCRIPT_AUTH_BACKEND=kv-oauth` and the provider env vars are set.
- [ ] `curl http://localhost:8094/health/ready` succeeds.
- [ ] `curl http://localhost:8094/api/v1/auth/me` returns `{ "authenticated": false }` before login.

> Local smoke without real credentials
>
> If provider env is missing, the
>
> kv-oauth
>
> backend falls back to a non-functional local-default endpoint set so the service still boots and
>
> /health
>
> ,
>
> session
>
> , and
>
> me
>
> answer. Real
>
> signin
>
> /
>
> callback
>
> require genuine provider credentials — the fallback is a stub path, not a working login.

## What you built

Your workspace now has an identity layer: the `auth` plugin composed onto the interactive `kv-oauth` backend, the `auth.prisma` migration applied, and the `auth-api` service answering on `:8094` with the five-route contract. You proved the session endpoints are wired — the app can now tell a signed-in team member from a stranger. Next you give it something worth protecting: its own isolated database for team records.

[1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/01-scaffold/) [3 · Workspace data](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/03-workspace-data/)
