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.
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 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:
# In my-workspace/, with `aspire start` up in another terminal
curl http://localhost:3001/health # the workspace service from chapter 1
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:
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 curls 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:
netscript plugin list
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.
| 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:
export NETSCRIPT_AUTH_BACKEND=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:
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
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:
# 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.
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):
// 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'
// 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:
| 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:
# 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:
# 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 listshows theauthplugin. - [ ]
netscript db statusreports theauth.prismamigration applied. - [ ]
NETSCRIPT_AUTH_BACKEND=kv-oauthand the provider env vars are set. - [ ]
curl http://localhost:8094/health/readysucceeds. - [ ]
curl http://localhost:8094/api/v1/auth/mereturns{ "authenticated": false }before 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.