Configuration

Access Token

Gate every inspector endpoint behind a short-lived token.

Access Token

Nest Graph Inspector installs its endpoints on a native HTTP server inside your running application. Those endpoints expose your application's internal structure, and the Direct Run endpoint invokes live provider or controller methods, so by default every one of them is gated behind a short-lived access token.

Token protection is on unless you set accessToken.enabled to false. You do not need to configure anything.

accessToken?: {
  enabled?: boolean
  ttlMs?: number
  secret?: string
  logToken?: boolean
  bruteForce?: { /* see below */ }
}

What a token gates

Tokens gate every endpoint the inspector installs, and nothing else in your application. For a Viewer output, the graph routes below are installed; the Direct Run and history routes are installed only when that output's directRun.enabled is not false (it defaults to true):

EndpointWhat it serves
GET /__graph-inspector/information.jsonWhich inspector version is installed and what it serves
GET /__graph-inspector/output.jsonYour dependency graph
GET /__graph-inspector/output.schema.jsonThe JSON Schema that graph conforms to
GET /__graph-inspector/output.mdThe same graph as Markdown
POST /direct-runInvokes an advertised provider or controller method; exists only when Direct Run is enabled
GET /direct-run/historiesCompleted Direct Run traces in full; exists only when Direct Run is enabled
GET /direct-run/history/index.jsonA summary list of completed traces; exists only when Direct Run is enabled
GET /direct-run/history/<traceId>One completed trace; exists only when Direct Run is enabled

The graph paths above are relative to the Viewer output's path. /direct-run is the default Direct Run path and follows directRun.path if you change it. With directRun.enabled: false, the graph remains available but none of the Direct Run or history paths exist, and its graph omits Direct Run metadata. The AI chat is not on this list because it is not an endpoint: the model runs inside your browser and reads the graph through output.md, which is already gated.

How a token travels

A token is issued when your application starts and is embedded directly in the viewer link printed to your console, so opening that link just works. That link is the only thing that hands a token over: the viewer reads it once, keeps it for the tab, and immediately rewrites its own address bar without it. From then on it authenticates with the x-graph-inspector-token header on every follow-up request, Direct Run included.

That means the URL you can copy out of the viewer, share, or leave in your history is not a working credential — only the link printed in your console is.

When you call the endpoints yourself, send the token any of these ways:

curl -H 'Authorization: Bearer <token>' http://localhost:53371/__graph-inspector/output.json
curl -H 'x-graph-inspector-token: <token>' http://localhost:53371/__graph-inspector/output.json
curl 'http://localhost:53371/__graph-inspector/output.json?__inspector_token=<token>'

A request without a valid token is answered with 401 and a JSON body naming the reason: missing, malformed, signature, or expired. Two cases do not answer 401: a caller that has been locked out for repeated guesses gets 429 instead (see below), and a CORS preflight (OPTIONS) is not gated at all, because it carries no token of its own and blocking it would stop the browser from ever sending the request it is asking about.

Expiry

A token carries its own expiry. Issued at 13:00 with the default lifetime, it stops working at 16:00 — the payload is signed, so the expiry cannot be edited without invalidating the token.

Once a token expires, restart the application to print a fresh viewer link.

Can a token be guessed?

Not by trying. A token is accepted only if its HMAC-SHA256 signature checks out, which means guessing one means guessing a 256-bit value.

The part worth your attention is the signing secret. Left unset, it is 32 random bytes generated per process and there is nothing to guess. If you set secret yourself, keep it long: a token exposes both the payload it signs and the resulting signature, so anyone holding a single leaked token can test candidate secrets offline, on their own hardware, without ever touching your application. A short or dictionary secret falls quickly that way. The library warns at startup when a configured secret is under 32 characters.

The other real risk is leakage rather than guessing. A token placed in a URL travels into access logs, proxy logs, and browser history, so treat a token as you would a password and prefer the header form for anything scripted — which is what the viewer itself does.

The startup message is itself one of those places. It is the only channel that hands a token out, so it prints one by default, which means anything collecting your application logs collects a working credential along with it. Where that matters, set logToken: false: the token is then left out of the message and out of the printed viewer link, and you supply your own by configuring a secret and minting tokens with the exported AccessTokenService.

Brute force lockout

Repeated invalid tokens from the same client address are counted, and once a client runs out of attempts it is refused with 429 and a Retry-After header until the block expires.

accessToken?: {
  bruteForce?: {
    enabled?: boolean
    maxFailures?: number
    windowMs?: number
    blockMs?: number
    maxTrackedClients?: number
  }
}

A block applies to the client, not the token: once blocked, even a valid token is refused until the block lifts. Other clients are unaffected.

Two kinds of rejection are deliberately not counted as guesses:

  • a request with no token at all, which is not an attempt at one, and would otherwise lock out anyone who simply opened an endpoint in a browser
  • a request with a genuine but expired token, which could only have come from something that already held a real one

Clients are identified by socket address. A forwarded header such as X-Forwarded-For is intentionally ignored, since the caller sets it: trusting one would let a brute force rotate past the lockout, and would let anyone lock out an address of their choosing. If you place the inspector behind a reverse proxy, every request arrives with the proxy's address and the lockout becomes effectively global.

APIDescriptionDefault
enabled?: booleanWhether failed guesses are counted at all.true
maxFailures?: numberInvalid tokens allowed inside one window before the client is blocked.10
windowMs?: numberHow long failures accumulate before the count resets.60000
blockMs?: numberHow long a blocked client is refused.900000
maxTrackedClients?: numberUpper bound on tracked addresses, so the tracker cannot be grown without limit.1000

API

APIDescriptionBenefit
enabled?: booleanWhether inspector endpoints require a token. Defaults to true.Lets you opt out on a host where the inspector port is already unreachable from outside.
ttlMs?: numberHow long an issued token stays valid, in milliseconds. Defaults to three hours.Matches the token lifetime to how long a debugging session actually lasts.
secret?: stringSigning secret, 32 characters or more. Defaults to the NEST_GRAPH_INSPECTOR_TOKEN_SECRET environment variable, then to a random per-process secret.Keeps issued tokens valid across restarts, which matters for containers that reboot often.
logToken?: booleanWhether the issued token is printed on startup. Defaults to true.Keeps a live credential out of shipped application logs when set to false, at the cost of minting tokens yourself.
bruteForce?: objectPer-client lockout for repeated invalid tokens. See above.Caps how fast an attacker can try at all, and surfaces the attempt as a logged block.

Example

NestGraphInspectorModule.forRoot({
  outputs: [{ type: 'viewer' }],
  accessToken: {
    ttlMs: 30 * 60 * 1000,
    secret: process.env.INSPECTOR_SECRET,
  },
})
Turning enabled off removes the only check in front of the Direct Run endpoint, which executes provider or controller methods against your running application with its real database connections. Leave it on unless the port is unreachable from anywhere you do not control.
Nest Graph Inspector · MIT License · © 2026