Personal Developer Keys
User-bound nxd_ credentials for the Query and Trace HTTP APIs, separate from gateway API keys
Personal developer keys are user-bound credentials for CLI, SDK, and automation clients that need Nexus Query and Trace HTTP APIs without a browser session.
In the console they appear under Account → Personal developer keys (/account).
The auth service stores them as developer access tokens and issues secrets with the nxd_… prefix.
They are not gateway API keys (nxs_…) and not trace ingest keys.
Gateway keys send model traffic; personal keys read observability data (and optionally write request feedback) as the issuing user.
Deployed application (primary)
Section titled “Deployed application (primary)”On a Traefik-backed deployment, consumers and tenants reach Nexus through the published hostnames, not the internal service ports.
| Public hostname (env) | Typical role | Example |
|---|---|---|
NEXUS_CONSOLE_HOSTNAME |
Tenant console (UI + same-origin BFF) | https://console.example.com |
NEXUS_HOSTNAME |
Gateway edge (model traffic, /v1/traces) |
https://api.example.com |
Auth (auth:14453) and Query (query:14461) stay on the private Compose network.
The console container calls them with AUTH_HTTP_ENDPOINT and QUERY_HTTP_ENDPOINT.
Stock Traefik labels publish console and gateway (plus optional Grafana / path-routed trace ingest on the gateway host).
They do not publish auth or query directly.
What that means for you:
- Mint and revoke keys in the console UI on the console hostname (works with OIDC or password login, depending on deployment policy).
- Send model traffic to the gateway hostname with a gateway API key (
nxs_…). - Call Query/Trace with
nxd_…only against a Query HTTP base URL your operator has made reachable to you (published Query hostname, VPN, or private network). The stock public edge does not put Query on the gateway hostname.
Browser sessions in the console do not need a personal key: the console BFF under /api/query/** forwards the signed-in session cookie to Query on the private network.
# Set these to your deployment's public URLs.CONSOLE_URL="${CONSOLE_URL:-https://console.example.com}"GATEWAY_URL="${GATEWAY_URL:-https://api.example.com}"
# Query base URL only if your operator publishes Query HTTP to clients.# Stock Traefik compose leaves query on the private network.QUERY_URL="${QUERY_URL:-https://query.example.com}"What is available today
Section titled “What is available today”| Capability | Available to a deployed-app user |
|---|---|
| Create / list / revoke in the console UI | Yes |
| Org-wide or single-project scope | Yes |
| Expiry (default 30 days, bounded by the effective maximum key age) | Yes |
| Scoped permissions (see below) | Yes |
Authorization: Bearer nxd_… against Query HTTP (when Query is reachable) |
Yes |
| Use against the gateway for chat/completions | No — use nxs_… |
| Control-plane admin via console-minted scopes | No |
| Rotate in place (edit secret) | No — revoke and create a new key |
| Machine / service-account tokens (non-user) | No |
| Call Query via the public gateway hostname | No (gateway does not serve /api/requests, …) |
Call Query via console /api/query/** with an nxd_… bearer |
No — that BFF uses the browser session cookie |
Effective access on every use is the intersection of:
- Scopes stored on the token
- The user’s current org/project membership and role
- Token not revoked or expired; user still active
- Policy (PDP) allow for the specific read or feedback action
Demotion, membership removal, disablement, expiry, or revocation takes effect on the next verification. Personal keys are not subject to console session idle timeout.
Scopes
Section titled “Scopes”The console scope picker offers these values. Creation rejects any scope the caller’s effective role does not hold.
| Scope | Console label | Unlocks |
|---|---|---|
query:read |
Request metadata | Request rows, sessions, session timelines |
trace:read |
Trace metadata | Trace envelopes and safe tool summaries |
trace_payload:read |
Full trace payloads | Stored bodies and full tool payload refs when tracing captured them |
metrics:read |
Metrics | Usage, metrics overview, panel exports |
feedback:write |
Feedback | Submit request feedback from SDKs or automation |
Role ceilings for mintable scopes:
| Role | Can include trace_payload:read |
Notes |
|---|---|---|
manager |
Yes | Full set above |
auditor |
No | Metadata, metrics, feedback; no full payloads via personal keys |
user |
No | Same as auditor for this surface |
Full payload reads also require a successful trace_payload.read PDP decision and stored payload references.
Metadata reads remain available with query:read, trace:read, or metrics:read as appropriate.
The console does not offer control-plane scopes (control_plane:read, control_plane:write, …) on personal keys.
Endpoints that require those scopes (for example GET /api/reports/workspace) return HTTP 403 for typical nxd_ tokens even when the user could call them with a console session.
Credential families
Section titled “Credential families”| Prefix | Purpose | Where you send it |
|---|---|---|
nxd_… |
Personal developer key (this doc) | Query HTTP base URL (when published / reachable) |
nxs_… |
Gateway API key | Gateway hostname (/v1/…, /anthropic/…) |
| Trace ingest key | Project-scoped OTLP write | Gateway hostname /v1/traces (path-routed) |
Query HTTP rejects gateway API keys with HTTP 401 (gateway api keys cannot read query APIs).
Create a key in the console
Section titled “Create a key in the console”- Open the tenant console on your deployment’s console hostname (for example
https://console.example.com). - Sign in (OIDC and/or password, per that deployment’s auth config).
- Open Account (
/account). - Select the organization, then New key.
- Set name, optional project scope, expiry days, and permissions. The expiry field defaults to the lesser of 30 days and the effective maximum key age for the selected organization/project, and the dialog shows that maximum when one is configured.
- Copy the secret once — it is not shown again.
The raw secret is returned only on create. The Account list is a searchable, paginated table that defaults to Active (hides revoked and past-expiry keys); switch the state filter to Revoked / Expired / All when needed. List rows show name, prefix, project scope, permissions, status (Active / Expires in N days / Expired / Revoked), and last-used time. Revoke stays available for every status except already-revoked keys.
This is the path that works on OIDC-only deployments (password login disabled).
Calling Query HTTP with a personal key
Section titled “Calling Query HTTP with a personal key”Use the personal key as a bearer token against Query HTTP, not against the gateway hostname.
Every list/detail route requires org_id (and respects optional project_id filters and the token’s project grant).
QUERY_URL="${QUERY_URL:?set QUERY_URL to the Query HTTP base your operator published}"NEXUS_DEV_TOKEN="nxd_…" # secret shown once at create timeORG_ID="org_…"
# Recent requestscurl -sS "$QUERY_URL/api/requests?org_id=$ORG_ID&limit=20" \ -H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# One requestREQUEST_ID="req_…"curl -sS "$QUERY_URL/api/requests/$REQUEST_ID?org_id=$ORG_ID" \ -H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Sessionscurl -sS "$QUERY_URL/api/sessions?org_id=$ORG_ID&limit=20" \ -H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Session timelineSESSION_ID="sess_…"curl -sS "$QUERY_URL/api/sessions/$SESSION_ID/timeline?org_id=$ORG_ID" \ -H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Metrics overview (needs metrics:read)curl -sS "$QUERY_URL/api/metrics/overview?org_id=$ORG_ID" \ -H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Trace metadata for a request (needs trace:read)curl -sS "$QUERY_URL/api/traces/requests/$REQUEST_ID?org_id=$ORG_ID" \ -H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Stored body (needs trace_payload:read + stored body + PDP allow)curl -sS "$QUERY_URL/api/body/by-request/$REQUEST_ID?org_id=$ORG_ID&kind=response" \ -H "Authorization: Bearer $NEXUS_DEV_TOKEN"
# Feedback (needs feedback:write)curl -sS -X POST "$QUERY_URL/api/requests/$REQUEST_ID/feedback" \ -H "Authorization: Bearer $NEXUS_DEV_TOKEN" \ -H 'Content-Type: application/json' \ -d "$(jq -n \ --arg org "$ORG_ID" \ --arg project "$PROJECT_ID" \ '{org_id:$org, project_id:$project, score:1, label:"helpful", comment:"worked well"}')"score must be -1, 0, or 1.
Live session stream (SSE)
Section titled “Live session stream (SSE)”Requires query:read and membership for the org/session.
Project-scoped keys only see events for their project.
curl -N "$QUERY_URL/api/sessions/$SESSION_ID/live?org_id=$ORG_ID" \ -H "Authorization: Bearer $NEXUS_DEV_TOKEN" \ -H 'Accept: text/event-stream'Python example
Section titled “Python example”import osimport urllib.parseimport urllib.request
query_url = os.environ["QUERY_URL"] # operator-published Query HTTP basetoken = os.environ["NEXUS_DEV_TOKEN"] # nxd_…org_id = os.environ["ORG_ID"]
params = urllib.parse.urlencode({"org_id": org_id, "limit": 20})req = urllib.request.Request( f"{query_url}/api/requests?{params}", headers={"Authorization": f"Bearer {token}"},)with urllib.request.urlopen(req) as resp: print(resp.read().decode())TypeScript example
Section titled “TypeScript example”const queryUrl = process.env.QUERY_URL!; // operator-published Query HTTP baseconst token = process.env.NEXUS_DEV_TOKEN!; // nxd_…const orgId = process.env.ORG_ID!;
const url = new URL("/api/requests", queryUrl);url.searchParams.set("org_id", orgId);url.searchParams.set("limit", "20");
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` },});if (!response.ok) { throw new Error(`${response.status} ${await response.text()}`);}console.log(await response.json());Query routes personal keys can use
Section titled “Query routes personal keys can use”When the token carries the required scope(s) and Query HTTP is reachable, these routes accept nxd_… bearers:
| Method | Path | Typical scopes |
|---|---|---|
GET |
/api/requests |
query:read |
GET |
/api/requests/:request_id |
query:read |
GET |
/api/sessions |
query:read |
GET |
/api/sessions/:session_id/timeline |
query:read |
GET |
/api/sessions/:session_id/live |
query:read |
GET |
/api/usage |
metrics:read |
GET |
/api/metrics/overview |
metrics:read |
GET |
/api/exports/panel |
metrics:read |
POST |
/api/requests/:request_id/feedback |
feedback:write |
GET |
/api/body/by-request/:request_id |
trace_payload:read |
GET |
/api/traces/requests/:request_id |
trace:read (+ payload scope for full refs) |
GET |
/api/traces/sessions/:session_id |
trace:read |
GET |
/api/traces/tools/:request_id |
trace:read |
GET |
/api/traces/:trace_id/tree |
trace:read |
GET |
/api/traces/:trace_id/spans/:span_id |
trace:read |
GET |
/api/traces/:trace_id/artifacts/:artifact_id |
trace:read / trace_payload:read as applicable |
GET |
/api/requests/:request_id/trace |
trace:read |
GET |
/api/sessions/:session_id/traces |
trace:read |
GET |
/api/sessions/:session_id/traces/live |
trace:read |
Canonical route descriptions live in Wire Contracts.
GET /api/reports/workspace additionally requires control_plane:read, which console-minted personal keys do not receive.
Scripted lifecycle (optional)
Section titled “Scripted lifecycle (optional)”Minting and revocation require a console session, not an nxd_ key.
On a deployed app, prefer the console UI above—especially when password login is disabled and only OIDC is offered.
When password login is enabled, you can script against the console origin (auth is rewritten/proxied; session cookies are set by the console session routes):
CONSOLE_URL="${CONSOLE_URL:-https://console.example.com}"ORG_ID="org_…"
# Sign in and keep cookies (console session route).curl -sS -c nexus-cookies.txt -X POST "$CONSOLE_URL/api/session/sign-in" \ -H 'Content-Type: application/json' \ -d '{"email":"you@example.com","password":"your-password"}' >/dev/null
# Create (console BFF attaches the session cookie to auth).curl -sS -b nexus-cookies.txt -X POST "$CONSOLE_URL/api/auth/developer-tokens" \ -H 'Content-Type: application/json' \ -d "$(jq -n \ --arg org "$ORG_ID" \ --arg name "CI observability reader" \ '{ org_id: $org, project_id: null, name: $name, scopes: ["query:read","trace:read","metrics:read","feedback:write"], expires_at: "2026-12-31T00:00:00Z" }')"Successful create response shape:
{ "id": "dt_…", "token": "nxd_…", "prefix": "…", "org_id": "org_…", "project_id": null, "scopes": ["query:read", "trace:read", "metrics:read", "feedback:write"], "expires_at_unix": 1770000000}Store token immediately.
Only the hash and prefix are persisted.
Project-scoped create:
curl -sS -b nexus-cookies.txt -X POST "$CONSOLE_URL/api/auth/developer-tokens" \ -H 'Content-Type: application/json' \ -d "$(jq -n \ --arg org "$ORG_ID" \ --arg project "$PROJECT_ID" \ --arg name "CI project reader" \ '{ org_id: $org, project_id: $project, name: $name, scopes: ["query:read","metrics:read"], expires_at: "2026-12-31T00:00:00Z" }')"expires_at is an optional RFC3339 timestamp.
When it is omitted, the token expires after 30 days.
A workspace, organization, or project maximum key age — or NEXUS_MAX_KEY_AGE_DAYS — caps the requested timestamp and lowers the 30-day default; a request beyond the cap is rejected with HTTP 400 and {"message":"key expiration exceeds the maximum key age"}. With no cap configured, any future timestamp is accepted.
Create also returns 400 with a message for empty names or scopes the caller’s role cannot mint, and 403 with a message when the caller has no membership in the requested organization.
Those validation failures do not invalidate the console session.
List and revoke:
curl -sS -b nexus-cookies.txt \ "$CONSOLE_URL/api/auth/developer-tokens?org_id=$ORG_ID" | jq .
TOKEN_ID="dt_…"curl -sS -b nexus-cookies.txt -X DELETE \ "$CONSOLE_URL/api/auth/developer-tokens/$TOKEN_ID" \ -o /dev/null -w '%{http_code}\n'Auth also exposes the same lifecycle paths on its private HTTP listener (POST|GET /api/auth/developer-tokens, DELETE /api/auth/developer-tokens/{token_id}) for operators with network access to that service.
Failure modes
Section titled “Failure modes”| Situation | Typical result |
|---|---|
| Missing / bad bearer on Query | HTTP 401 |
Gateway nxs_… key on Query |
HTTP 401 |
Hitting /api/requests on the gateway hostname |
HTTP 404 (not a Query route there) |
| Scope missing for the route | HTTP 403 |
| Org/project outside membership or token grant | HTTP 403 |
| Requested create scopes exceed role | HTTP 400 {"message":"scope … is not available to your role"} |
| Create expiry exceeds maximum key age | HTTP 400 {"message":"key expiration exceeds the maximum key age"} |
| Create without membership in the org | HTTP 403 {"message":"no membership in organization"} |
| Revoke unknown or already-revoked token | HTTP 404 {"message":"developer token not found"} |
| Token revoked or expired | Verification fails |
| User disabled or membership removed | Verification fails / 403 |
| Full body when tracing did not store it | HTTP 404 (body not stored) |
Audit and correlation
Section titled “Audit and correlation”Create and revoke emit developer_token.created and developer_token.revoked.
Verified principals carry an internal developer_token:<id> scope for correlation.
Query reads emit data_access.* events (for example data_access.request_viewed, data_access.trace_payload_viewed) that include the token id without the raw secret.
last_used_at updates on successful verification.
Local contributor stack
Section titled “Local contributor stack”When you run the stock local Compose stack with published host ports (see Ports), the same APIs are also reachable as http://127.0.0.1:14449 (console), http://127.0.0.1:14453 (auth), and http://127.0.0.1:14461 (query).
Prefer the deployed hostname model above when writing client integrations; use localhost only for contributor debugging on a machine that can reach those ports.
Related docs
Section titled “Related docs”- Services — auth, query, and console responsibilities
- Wire Contracts — Query HTTP surface
- Ports — local Compose ports
- User Glossary — operator-facing terms
- Observability (Local Stack) — Traefik hostname defaults for local stacks
- End-to-End Walkthrough — minting a gateway
nxs_…key and sending a chat request - Client Integrations — point coding clients at the gateway with
nxs_…(not personal keys)
