Skip to main content
0.0.x

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.)

Prerequisites
NameTypeDescription
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.

Auth backends — capability matrix (NETSCRIPT_AUTH_BACKEND)
NameTypeDescription
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:

auth.prisma models → your database tables
NameTypeDescription
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/*):

auth-api endpoints (:8094, /api/v1/auth/*)
NameTypeDescription
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.

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. createAuthServiceAuthenticator verifies the request's bearer session against the auth service found through discovery. The plugin needs no auth backend, KV handle, or provider secret. A missing or rejected session returns 401. If the auth service cannot be reached, the request returns 503.
  • Authorization. createContractAuthorizer reads the scopes each procedure declares, for example .meta({ access: { authentication: 'required', authorization: { scopes: ['billing:read'] } } }) on the contract route. A session without that scope returns 403. Pass the same PluginContractMount to router assembly and to mountPluginContract so REST paths and RPC keys line up.
  • Health. /health stays 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