Add authentication
Scope. This recipe adds sign-in, sessions, and a /me identity endpoint to an existing
NetScript workspace by installing the official auth plugin. You will choose an
authentication backend, set the backend's environment, run the auth database migration, and
verify a live session through the generated Fresh app on its Aspire-discovered origin. By the end you have a
working OAuth/OIDC sign-in flow on the default backend (kv-oauth) and a clear picture of what
the two non-interactive backends (workos, better-auth) do and do not provide.
This is the task-oriented companion to the authentication capability hub (the headline API and endpoint map) and the authentication model explanation (why the backend is a pure adapter behind a port). If you want the why, read those; if you want the how, stay here.
Before you start
You need an existing workspace with a database, because the kv-oauth and better-auth backends
persist sessions and accounts. The auth plugin sets requiresDb: true and requiresKv: true, so
both a database and KV (Redis) must be in the Aspire graph. The database is polyglot — Postgres is
the recommended default, but mysql, mssql, or sqlite are first-class alternatives selected at
scaffold time with netscript init --db <engine>. (Postgres/MySQL/SQL Server run as an Aspire
container resource; SQLite is file-backed with no container.)
| Name | Type | Description |
|---|---|---|
Workspace |
netscript init |
An existing project. If you have none, scaffold one first — see the tutorials. |
netscript CLI |
on PATH |
Installed globally: deno install --global --allow-all --name netscript jsr:@netscript/cli@0.0.8. Confirm with netscript --help. |
Aspire |
aspire start |
Configure/export auth environment, then start the AppHost before any db command or endpoint call. |
OAuth credentials |
client id / secret |
For the default kv-oauth backend you need a real OAuth/OIDC app (e.g. a Google client id + secret + redirect URI). Without provider env, signin/callback are non-functional stubs. |
Throughout, run commands from your workspace root.
Step 1 — Add the auth plugin
The auth plugin is a first-class official plugin installed the same way as workers, sagas,
triggers, and streams. Add it with plugin install:
netscript plugin install @netscript/plugin-auth --port 8094
This installs the unified @netscript/plugin-auth dependency, emits the user-owned auth/mod.ts
glue barrel, and registers it. The plugin package composes one active backend behind the
auth-api oRPC service and contributes the Prisma schema (auth.prisma), service entry, and
/api/v1/auth/* routes.
Step 2 — Choose a backend with the auth CLI
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.
| 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 cookie under NETSCRIPT_AUTH_COOKIE_NAME (default __Host-ns_session). The CLI WorkOS preset explicitly selects wos-session to match AuthKit. Subject-wide logout uses a durable KV fence and background upstream cleanup. Validates an existing session; signin/callback return AUTH_PROVIDER_ERROR (no interactive flow). 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. |
For the rest of this recipe we use kv-oauth. Persist the choice in the workspace boot seam and
confirm what the service will use:
netscript plugin auth backend set kv-oauth
netscript plugin auth backend show
netscript plugin doctor
The command reconciles NETSCRIPT_AUTH_BACKEND in the project .env and the canonical plugin
selector in appsettings. Export the environment before starting the AppHost, as shown in Step 3. Directly setting the environment variable or the
auth.backend / Auth.Backend appsettings key remains an escape hatch for deployment systems that
own configuration externally.
Step 3 — Configure the provider and secrets
Each backend reads its own environment block. The auth CLI owns the normal setup path and writes the
same project .env seam as Step 2. For GitHub on kv-oauth:
# Your credential source exports NETSCRIPT_AUTH_CLIENT_SECRET.
# Keep generated encryption material in the process environment too.
export NETSCRIPT_AUTH_KV_OAUTH_KEY="$(netscript plugin auth secret generate kv-oauth-key)"
netscript plugin auth provider set \
--preset github \
--client-id "$NETSCRIPT_AUTH_CLIENT_ID" \
--redirect-uri "$NETSCRIPT_AUTH_REDIRECT_URI"
set -a
. ./.env
set +a
GitHub is OAuth 2.0, so the preset emits no NETSCRIPT_AUTH_ISSUER and ignores --issuer.
The CLI prints a notice when --issuer is ignored. Re-running the command removes an issuer saved
by an older preset. The runtime also ignores an issuer inherited from an old shell or deployment
when NETSCRIPT_AUTH_PROVIDER_ID=github. GitHub does not serve an OIDC
discovery document; sign-in uses these explicit endpoints and derives the stable subject
github:<id> from userinfo instead:
NETSCRIPT_AUTH_PROVIDER_ID=github
NETSCRIPT_AUTH_AUTHORIZATION_ENDPOINT=https://github.com/login/oauth/authorize
NETSCRIPT_AUTH_TOKEN_ENDPOINT=https://github.com/login/oauth/access_token
NETSCRIPT_AUTH_USERINFO_ENDPOINT=https://api.github.com/user
NETSCRIPT_AUTH_SCOPES=read:user user:email
Provider credentials and settings are written only to the project .env, which must stay outside
version control. Tracked appsettings receives the non-secret backend selector; reconciliation prunes
legacy credential copies and retains unrelated benign environment settings. Aspire refuses declared
credential-shaped keys as source literals, so generated helpers do not carry their values. Earlier
provider configuration copied credentials into tracked files; remove those copies and rotate any
credential that was committed.
Generated auth assignments are POSIX shell literals. Source and export the file before starting
Aspire so its executable resources inherit the exact values. Apostrophes, substitutions, backticks,
backslashes and embedded newlines remain data. Deno's direct --env-file loader accepts simple
quoted values and scopes but does not implement all POSIX quoting; for hostile values, use the
source/export path above. Provider credential environment bindings avoid putting secrets in CLI
arguments. Explicit credential flags remain supported for compatibility.
Tenant presets such as okta, auth0, azure-ad, aws-cognito, logto, and clerk additionally
accept --issuer. The non-interactive variants use their boot-native credential names:
# Your credential source exports WORKOS_API_KEY and WORKOS_COOKIE_PASSWORD.
netscript plugin auth provider set --preset workos --client-id "$WORKOS_CLIENT_ID"
export BETTER_AUTH_SECRET="$(netscript plugin auth secret generate better-auth)"
netscript plugin auth provider set --preset better-auth
The explicit exports below are the escape hatch for CI/deployment systems that inject environment variables themselves. Ordinary workspace setup uses the source/export step above.
# Selects the interactive OAuth/OIDC backend
export NETSCRIPT_AUTH_BACKEND=kv-oauth
# Provider credentials (e.g. a Google 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
# OIDC discovery / endpoints (preset providers fill these for you)
export NETSCRIPT_AUTH_ISSUER=https://accounts.google.com
export NETSCRIPT_AUTH_AUTHORIZATION_ENDPOINT=https://accounts.google.com/o/oauth2/v2/auth
export NETSCRIPT_AUTH_TOKEN_ENDPOINT=https://oauth2.googleapis.com/token
export NETSCRIPT_AUTH_USERINFO_ENDPOINT=https://openidconnect.googleapis.com/v1/userinfo
export NETSCRIPT_AUTH_SCOPES=openid email profile
# Stable subject: OIDC providers use the ID-token sub. A preset named by
# NETSCRIPT_AUTH_PROVIDER_ID supplies its own default (github: userinfo id).
# export NETSCRIPT_AUTH_SUBJECT_SOURCE=userinfo # id_token | userinfo
# export NETSCRIPT_AUTH_SUBJECT_CLAIM=id
# Optional: cookie + KV tuning
export NETSCRIPT_AUTH_COOKIE_NAME=__Host-ns_session
export NETSCRIPT_AUTH_KV_OAUTH_KEY=<base64url-encoded-32-byte-secret> # required for kv-oauth: missing key material is a startup error
# Local HTTP only: export NETSCRIPT_AUTH_ALLOW_INSECURE_HTTP_REQUESTS=true
# TLS proxy only: export NETSCRIPT_AUTH_TRUST_PROXY_HEADERS=true
# Direct TLS at the service: export NETSCRIPT_AUTH_COOKIE_SECURE=true
# Outbound HTTP provider tests only: export NETSCRIPT_AUTH_ALLOW_INSECURE_REQUESTS=true
export PORT=8094
export NETSCRIPT_AUTH_BACKEND=workos
export NETSCRIPT_AUTH_COOKIE_NAME=__Host-ns_session
# Configure the AuthKit issuer with the same cookie name.
# Bound retained access tokens by the longest configured WorkOS duration.
export WORKOS_ACCESS_TOKEN_MAX_LIFETIME_MS=86400000
export WORKOS_API_KEY=sk_...
export WORKOS_CLIENT_ID=client_...
export WORKOS_COOKIE_PASSWORD=at-least-32-characters-of-entropy
# signin/callback return AUTH_PROVIDER_ERROR on this backend.
# It validates an existing WorkOS AuthKit session (session/me).
export PORT=8094
export NETSCRIPT_AUTH_BACKEND=better-auth
export BETTER_AUTH_SECRET=at-least-32-characters-of-entropy
export DB_PROVIDER=postgres
# Persists users/sessions/accounts via auth.prisma (Step 3).
# signin/callback return AUTH_PROVIDER_ERROR — validate-only.
export PORT=8094
Step 4 — Start Aspire and run the auth database migration
The auth plugin contributes a package-provided auth.prisma schema, which is
aggregated into your project's database schema at db generate (Postgres is the recommended engine; or mysql /
mssql / sqlite — the auth models persist through Prisma, so they follow whichever engine you
scaffolded with --db). It defines four better-auth-shaped models mapped to these tables:
| Name | Type | Description |
|---|---|---|
AuthUser |
auth_users |
Authenticated principals. Populated by backends that persist users (better-auth). |
AuthSession |
auth_sessions |
Server-side session records. kv-oauth keeps sessions in KV; this table backs the Prisma-persisting backend. |
AuthAccount |
auth_accounts |
Linked provider accounts (the OAuth/OIDC identities behind a user). |
AuthVerification |
auth_verifications |
Verification / challenge records used during account flows. |
After Step 3 has configured and exported the provider environment, start Aspire from the workspace root:
(cd aspire && aspire start)
If an AppHost was already running, stop it with (cd aspire && aspire stop) before this start
command so its replacement inherits the exported values.
The dashboard is at https://localhost:18888. With Aspire running, generate and apply the migration the same way you do for any plugin schema:
netscript db init --name init # first time only — create the migration
netscript db generate # generate Prisma client + Zod schemas from the aggregated schema
netscript db seed # optional seed data
netscript db status # confirm the migration is applied
See Run a database migration for the full DB workflow and the Aspire-up dependency.
Step 5 — The kv-oauth happy path (code)
When you compose the backend in code (for a custom service entry, a test, or a non-scaffold wiring),
the interactive kv-oauth backend is one await call. Pass a provider preset from providers.*:
import { createKvOAuthBackend, providers } from "@netscript/auth-kv-oauth";
// providers.google(...) is a preset that fills the OIDC endpoints for you.
const backend = await createKvOAuthBackend({
provider: providers.google({
clientId: Deno.env.get("NETSCRIPT_AUTH_CLIENT_ID")!,
clientSecret: Deno.env.get("NETSCRIPT_AUTH_CLIENT_SECRET")!,
redirectUri: "http://localhost:8094/api/v1/auth/callback",
}),
});
// backend implements AuthBackendPort AND the optional InteractiveFlowPort
// (signIn / handleCallback / getSessionId / signOut), so the auth-api
// signin + callback endpoints are live on this backend.
console.log(backend.name); // "kv-oauth"
import { createKvOAuthBackend, providers } from "@netscript/auth-kv-oauth";
// GitHub instead of Google — same shape, different preset.
const github = await createKvOAuthBackend({
provider: providers.github({
clientId: Deno.env.get("NETSCRIPT_AUTH_CLIENT_ID")!,
clientSecret: Deno.env.get("NETSCRIPT_AUTH_CLIENT_SECRET")!,
redirectUri: "http://localhost:8094/api/v1/auth/callback",
}),
});
// Tenant providers (auth0, okta, azureAd, awsCognito, logto, clerk) take
// a tenant/domain in addition to the client credentials.
The returned backend satisfies the AuthBackendPort seam that @netscript/plugin-auth-core
defines, and — because it is kv-oauth — also the optional InteractiveFlowPort. That is precisely
what makes signin and callback work on this backend and fail loud on the other two. For the port
architecture behind this, read the authentication model.
Step 6 — Start the service and the auth endpoints
With Aspire running, the auth-api service answers on host port 8094 (pinned in Step 1) and
mounts six endpoints under the
public REST prefix /api/v1/auth/* (the oRPC surface is mirrored at /api/rpc/v1/auth/*):
| Name | Type | Description |
|---|---|---|
POST /api/v1/auth/signin |
interactive only |
Begin the OAuth/OIDC redirect flow. Live on kv-oauth; returns AUTH_PROVIDER_ERROR on workos/better-auth. |
POST /api/v1/auth/callback |
interactive only |
Complete the provider redirect, mint a session. Live on kv-oauth; AUTH_PROVIDER_ERROR on the others. |
POST /api/v1/auth/signout |
session |
End the caller's own session. Needs the session cookie or a bearer credential (401 otherwise); everywhere: true ends all of the caller's sessions. On kv-oauth the response carries the session-clearing Set-Cookie. |
POST /api/v1/auth/sessions/revoke |
operator |
Revoke any session by id. Needs a credential holding the auth:sessions:revoke scope (403 otherwise). |
GET /api/v1/auth/session |
session |
Return the current session if one is present and valid. Works on all backends. |
GET /api/v1/auth/me |
identity |
Return the authenticated principal (the resolved user). Works on all backends. |
The service also exposes liveness/readiness probes at /health/live and /health/ready, plus
OpenAPI docs, through the standard @netscript/service builder. Watch it come up in the Aspire
dashboard at https://localhost:18888 under the auth-api resource.
Step 7 — Sign in through the generated app
Installing auth into a generated Fresh workspace emits auth/bff.ts,
auth/service.ts, and apps/<app>/routes/auth/[action].ts. Helper regeneration
also wires newly added services to the same auth resource. Register the provider
callback URI as <app-origin>/auth/callback, using the app endpoint reported by
Aspire. No fixed app port is required.
Submit a same-origin HTML form with method="post" and action="/auth/signin".
The app forwards the signin operation to the existing auth plugin and follows
its provider authorization redirect. The provider returns to the app's GET
/auth/callback route, which submits the code, state, and transaction cookie as
typed callback input to the plugin. The app response sets the first-party
HttpOnly session cookie and redirects to the app. GET /auth/session then
returns authenticated and subject, without exposing the session id. POST
/auth/signout requires the app's Origin, revokes the session, and expires the
cookie.
Use HTTPS in production. The generated local adapter issues a Secure
__Host-ns_session cookie on localhost, which browsers treat as a secure
cookie host even with a local HTTP endpoint. Other HTTP hosts are refused. If
you configure NETSCRIPT_AUTH_COOKIE_NAME, export the same value for the app
and auth service before starting Aspire. Authored routes and service policies
are preserved during regeneration; custom app layouts must mount the adapter
themselves.
For a guarded server-side read, attach the generated client to the service contract and supply only the incoming request's credential context:
import { browserSessionContext, createBrowserSessionClient } from './auth/bff.ts';
import { CatalogContractV1 } from './contracts/catalog/v1/mod.ts';
declare const request: Request;
const catalog = createBrowserSessionClient(
CatalogContractV1,
'catalog',
'catalog',
);
const result = await catalog.list({}, {
context: browserSessionContext(request),
});
The generated bearer contribution uses direct-only caching: credentials and
authenticated responses never enter a shared query cache. Services verify the
forwarded bearer through the existing remote authenticator; browser cookies are
never forwarded to guarded services. The scaffold's demonstration procedures
declare access: { authentication: 'none' } and remain public on REST and RPC.
/api/openapi.json and /api/docs also stay public for discovery. Other /api
paths require a bearer and a contract authorization decision once the CLI installs
its generated policy. Change the demo procedure metadata when making those routes private,
and keep browser reads in server-side app handlers. Workers, sagas, and triggers
use a service identity independently of browser sessions.
Generated CORS origins include enabled workspace apps and extend each
service/plugin's declared NETSCRIPT_CORS_ORIGINS. This is a response
allowlist; it does not authorize cookie callers. Publish-mode endpoint
references still require a deployment's public app origin configuration; this
local scaffold recipe does not certify deployment or browser conformance.
Direct auth service diagnostics
Confirm the service is up and the session endpoint responds. 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
The kv-oauth HTTP surface returns JSON redirect fields and emits the backend's
Set-Cookie headers on both REST and RPC. signin returns redirectUrl plus
the transaction cookie; callback returns redirectTo plus the session cookie.
Your application follows those JSON redirects and posts the provider's code
and state to the callback endpoint. The callback is POST-only: configure your
application callback route to perform that POST when the provider redirects back
with a GET. For the approved BFF topology, the app server owns these
browser-facing routes and sets the first-party cookie on its own origin. It forwards a bearer credential to
services; browser requests do not use credentialed cross-origin CORS. See
session lifecycles.
For this plain-HTTP loopback recipe, set both development overrides in the host environment
before starting or restarting auth-api. Use an unprefixed cookie name: clients reject
an insecure __Host- cookie even when server-side allowInsecureDev permits issuance.
Keep your real provider credentials and redirect URI from Step 3.
export NETSCRIPT_AUTH_ALLOW_INSECURE_HTTP_REQUESTS=true
export NETSCRIPT_AUTH_COOKIE_NAME=ns_session_dev
Then save the transaction cookie, visit the returned redirectUrl, and copy the provider
callback's code and state into the callback POST. The provider must register the redirect URI
from Step 3; its GET callback may report a method error on this POST-only service route.
Copy the code and state from that redirect URL and submit them below:
# Save the transaction cookie and read redirectUrl from the JSON response.
curl -c cookies.txt -X POST http://localhost:8094/api/v1/auth/signin \
-H 'Content-Type: application/json' -d '{}'
# Visit redirectUrl and authenticate with the provider, then submit its code/state.
# Send the transaction cookie and replace it with the issued session cookie.
curl -b cookies.txt -c cookies.txt -X POST http://localhost:8094/api/v1/auth/callback \
-H 'Content-Type: application/json' \
-d '{"code":"PROVIDER_CODE","state":"PROVIDER_STATE"}'
# Resolve the active session using only the cookie; no sessionId is needed.
curl -b cookies.txt http://localhost:8094/api/v1/auth/session
curl -b cookies.txt http://localhost:8094/api/v1/auth/me
# On kv-oauth, revoke the session and expire the cookie in the jar.
curl -b cookies.txt -c cookies.txt -X POST http://localhost:8094/api/v1/auth/signout \
-H 'Content-Type: application/json' -d '{}'
Use HTTPS for production cookies and remove the two development overrides above. The default
production cookie is __Host-ns_session; the local recipe uses ns_session_dev so curl can
save and resend it over plain HTTP. NETSCRIPT_AUTH_COOKIE_NAME configures the name used by
both the backend and service. A caller without a cookie jar may supply the transaction id as callback
input txn; a callback without either fails with oauth_cookie_missing.
A successful GET /api/v1/auth/session after callback returns the active
session; GET /api/v1/auth/me returns the authenticated principal. The callback
returns { completed, redirectTo, subject } without sessionId. The generated
BFF preserves the backend cookie on the app origin and uses it to forward a
bearer credential to services; browser callers use that first-party cookie.
0.0.8 cookie migration
Projects with their own backend, session routes, and browser bridge can opt out of generated browser
topology before installing auth or running lifecycle commands. Set
NetScript.Apps.<app>.Auth.BrowserTopology to "authored" in appsettings.json. The default
remains "scaffold". Authored apps keep their /auth routes and browser helper files untouched,
even when those files do not exist; generated service-session example routes are also skipped.
netscript generate aspire preserves that topology and netscript plugin doctor reports its
authored ownership. Task, Tauri, and desktop entries do not trigger browser helpers alongside an
authored browser app; workspaces without browser apps retain the legacy helper generation behavior.
Other scaffold browser apps still use shared helpers, and independently scaffold-owned service
policies still receive their bearer verifier. The setting does not delete previously generated
files. See app-owned browser authentication for
the configuration example and mixed-workspace behavior.
The callback input now accepts optional txn. The callback JSON output no
longer includes sessionId. This is a breaking contract change: replace reads of callback.sessionId with a cookie jar and an
empty-input session request. The app server must keep the issued cookie on its
own origin and forward a bearer credential to services. Server-only integrations
that compose a backend directly may use InteractiveFlowPort.handleCallback
and its session result; do not expose that credential in browser callback JSON.
Cookie headers are preserved on both HTTP projections. The public httpOnly
option now accepts only true or omission; remove false overrides.
Cookie issuance also refuses httpOnly: false from untyped callers and refuses insecure
cookies outside allowInsecureDev, including custom cookie names. Treat this
security policy tightening as a breaking change: remove insecure production
cookie overrides, use HTTPS, and keep HttpOnly enabled. __Host- cookies must
retain Path=/ and omit Domain, even during development.
Sign out with the same cookie. Signout only ever ends the caller's own sessions: without a
credential it returns 401, and a sessionId that is unknown or belongs to someone else returns
the same 401. See signout ownership.
# End this session
curl -b cookies.txt -X POST http://localhost:8094/api/v1/auth/signout
# End every session of the signed-in subject
curl -b cookies.txt -X POST -H 'content-type: application/json' \
-d '{"everywhere":true}' http://localhost:8094/api/v1/auth/signout
For a typed service-client call, use the auth/sdk-client.ts module emitted during install. The
manifest only advertises the factory; it never auto-attaches credentials. Select the generated
descriptor on the auth-api client and provide its declared context explicitly:
import { createServiceClient } from '@netscript/sdk/client';
import { authContract } from '@netscript/plugin-auth-core/contracts/v1';
import { authSdkClientContribution } from './auth/sdk-client.ts';
declare const currentSession: { accessToken: string; accountId: string };
const authClient = createServiceClient({
contract: authContract,
serviceName: 'auth-api',
routerName: 'auth',
contributions: [authSdkClientContribution] as const,
});
const session = await authClient.session(undefined, {
context: {
auth: { getAccessToken: () => currentSession.accessToken },
authCachePartition: currentSession.accountId,
},
});
signin, callback, and describe are explicitly public and do not resolve the credential.
session, me, signout, and revokeSession require it. The generated resolver reads no ambient environment,
cookie, or browser storage; your application supplies the credential for each logical call. Keep
authCachePartition stable and non-secret—never use a token, session id, email, or another
reversible identifier. Bearer headers require HTTPS outside localhost and loopback development.
Protect a plugin API
Every plugin service built with createPluginService from @netscript/plugin/service must choose
a posture in its auth field. Leaving the field out does not type-check, and a malformed posture
throws before the service starts. netscript plugin new <name> generates the guarded posture, and
it is the one to keep for any API that returns user data:
// plugins/billing/services/src/main.ts
import { createPluginService } from '@netscript/plugin/service';
import { mountPluginContract } from '@netscript/plugin/contract-base';
import { createAuthServiceAuthenticator } from '@netscript/plugin-auth/authenticator';
import { createContractAuthorizer } from '@netscript/service/auth';
import { billingContractDefinition } from '@netscript/plugin-billing-core/contracts/v1';
import { billingContractMount, billingRouter } from './handlers.ts';
export const billingService = createPluginService(billingRouter, {
name: 'billing',
auth: {
authn: {
authenticator: createAuthServiceAuthenticator({ serviceName: 'auth', timeoutMs: 10_000 }),
},
authz: {
authorizer: createContractAuthorizer(
mountPluginContract(billingContractDefinition, billingContractMount),
),
},
},
});
- Authentication.
createAuthServiceAuthenticatorverifies the request's bearer session against theauthservice found through discovery. The plugin needs no auth backend, KV handle, or provider secret. A missing or rejected session returns401. If the auth service cannot be reached, the request returns503. - Authorization.
createContractAuthorizerreads the scopes each procedure declares, for example.meta({ access: { authentication: 'required', authorization: { scopes: ['billing:read'] } } })on the contract route. A session without that scope returns403. Pass the samePluginContractMountto router assembly and tomountPluginContractso REST paths and RPC keys line up. - Health.
/healthstays anonymous. A healthy plugin does not prove that reads are authorized.
To publish an API on purpose without a guard, record the decision instead of omitting it:
export const statusService = createPluginService(statusRouter, {
name: 'status',
auth: { public: true, reason: 'Unauthenticated status API for the load balancer' },
});
The reason must not be blank, and a public posture cannot also carry authn or authz.
The exact posture types and validation rules are in the plugin authentication reference.
Production pitfalls
See also
The auth-api service, the five endpoints, and the three-backend capability matrix in one hub.
The authentication modelWhy the backend is a pure adapter behind AuthBackendPort, and how the plugin composes one active backend.
service referenceThe @netscript/service builder that auth-api is built on — RPC mount, health, OpenAPI, service info.
Run a database migrationThe full db init / generate / seed / status workflow that applies auth.prisma.