Skip to content

Installation

This document walks through running the Nexus gateway against a real OpenAI key on a local machine: bootstrap one user, workspace, organization, project, Nexus API key, and BYOK provider key; use the gateway from just dev, send one curl, and inspect the audit event the gateway emitted.

  • One Postgres instance (or container) reachable from your machine. The dev Compose stack in deploy/compose/ provisions both nexus_control and nexus_auth; any reachable Postgres with those databases migrated works.
  • A local KMS wrapping key persisted under --state-dir (default .nexus/ on the host, or /var/lib/nexus when using just cli against the dev compose stack). Wipe the directory and drop the nexus_control and nexus_auth databases to start over (or just dev-clean for compose volumes).
  • Multi-tenant flows and the full audit service pipeline are out of scope here. The local gateway writes audit events to audit.log; the standalone nexus-gateway binary also publishes to NATS subject audit.events when NATS_URL is configured.
Tool Minimum version Notes
Rust 1.96.1 rustup toolchain install 1.96.1 (auto via rust-toolchain.toml)
Docker any For Postgres / Compose if you don’t run your own server
curl any For the live request
jq any Optional, used to pretty-print JSON

You also need a real OpenAI API key with access to gpt-5.4-mini.

nexus reads and writes:

Location Purpose
Postgres database (NEXUS_DATABASE_URL) Orgs, projects, memberships, API keys, wrapped provider keys, model registry, control-plane audit_events_staging.
Postgres database (AUTH_DATABASE_URL) Auth users, identities, sessions, auth-side audit_events_staging (e.g. user.created).
--state-dir/control-plane.kms.key 32-byte AES-256 wrapping key for provider-key plaintext. Mode 0600.
--state-dir/audit.log One JSON-encoded audit event per line for the local gateway-only test.

The state directory is gitignored (host) or lives in the nexus-state compose volume. To wipe state, drop both databases and remove the volume (see Step 9) or rm -rf .nexus/ when you used a host-only CLI.

Step 0 — Bring up Postgres and run the CLI

Section titled “Step 0 — Bring up Postgres and run the CLI”
Terminal window
# Postgres + nexus_control / nexus_auth via the dev compose stack (Postgres is not published to localhost in stock compose).
just dev
# Run the CLI on the compose network (same DBs + `/var/lib/nexus` state as the gateway):
just cli help

The just cli … recipe runs cargo run -p nexus-cli inside a one-off container with NEXUS_DATABASE_URL / AUTH_DATABASE_URL pointing at postgres:5432 and NEXUS_STATE_DIR=/var/lib/nexus (shared with the gateway volume). Use -- only when you need to disambiguate flags for cargo vs nexus; for most commands, just cli orgs list is enough.

If you prefer a release binary on your host, run cargo build --release -p nexus-cli from the repository root and put target/release on your PATH, but you must then reach Postgres from the host (publish 5432 in a compose override or tunnel) and set NEXUS_DATABASE_URL / AUTH_DATABASE_URL accordingly — or use just cli-local …, which is plain cargo run -p nexus-cli -- … with your shell env.

nexus --help lists the auth, workspaces, orgs, projects, credentials, registry, and other subcommands.

If you’d rather not put a host build on your PATH, substitute ./target/release/nexus for nexus when you follow this doc with a fully host-local Postgres + .nexus/ layout instead of just cli.

The CLI talks to both databases: control-plane mutations use NEXUS_DATABASE_URL; nexus auth users ... uses AUTH_DATABASE_URL for password hashes and also opens the control DB when --org and optionally --project attach a membership.

Area Notes
Users nexus auth users create --email you@example.com [--name ...] (--password-stdin | --password-env VAR) [--org SLUG [--project SLUG] [--role manager]] mints a user and optionally a membership. nexus auth users list [--include-inactive] [--org SLUG]. nexus auth users disable <email-or-id> (rm alias) sets disabled_at (blocks login). nexus auth users enable <email-or-id> re-enables a disabled account.
Orgs / projects nexus orgs list / projects list hide soft-deleted rows unless --include-inactive. nexus orgs rm <ORG_REF> [--cascade] and nexus projects rm <ORG_REF/PROJECT_REF> [--cascade] set deleted_at.
API keys nexus credentials api-keys list hides revoked keys unless --include-inactive. nexus credentials api-keys rm <ak_…> (or lookup prefix) sets revoked_at.
Provider keys nexus credentials provider-keys list hides revoked unless --include-inactive. nexus credentials provider-keys rm <org/project/provider> revokes the active row for that triple.

Passwords must never be passed as argv; use --password-stdin or --password-env.

Terminal window
set -a
source .env
set +a

Do not pass the secret as a CLI argument; nexus credentials provider-keys create reads it from stdin or from a named env var so it never appears in argv (and therefore not in ps).

Step 2 — Bootstrap identity, workspace, org, and project

Section titled “Step 2 — Bootstrap identity, workspace, org, and project”

The browser-first path matches the product flow: open http://127.0.0.1:14449/sign-up and create a user. Sign-up provisions a personal workspace, a default organization, a default project, and workspace/org/project manager memberships for that user. Use the CLI to discover the generated slugs:

Terminal window
just cli orgs list
just cli projects list --org <org_slug>

For a headless CLI-only bootstrap, create the user first, then a shared workspace. workspaces create grants that user workspace manager access and creates a default organization and default project under the workspace:

Terminal window
export NEXUS_DEV_PASSWORD='change-me-local-password'
USER_ID=$(just cli auth users create \
--email dev@example.com \
--name "Acme Manager" \
--password-env NEXUS_DEV_PASSWORD)
just cli workspaces create --slug acme --name "Acme" --created-by "$USER_ID"
just cli orgs list
just cli projects list --org acme-default

Set ORG_SLUG to the organization slug from the browser-first or CLI-only path before continuing:

Terminal window
export ORG_SLUG=<org_slug>
export PROJECT_SLUG=default
Terminal window
TOKEN=$(just cli credentials api-keys create \
--org "$ORG_SLUG" \
--project "$PROJECT_SLUG" \
--name local-dev \
--issued-by "$(whoami)")
echo "$TOKEN"

The full nxs_<prefix>_<secret> token is the only copy that will ever exist in plaintext. Store it (or pipe it into a file) right now; the CLI cannot recover it later.

Terminal window
printf '%s' "$OPENAI_API_KEY" | just cli credentials provider-keys create \
--org "$ORG_SLUG" \
--project "$PROJECT_SLUG" \
--provider openai \
--name primary \
--secret-stdin

The stdin form works with the host one-shot just cli ... recipe because the secret is piped into the compose CLI container. Do not source .env inside the interactive just cli shell: the compose CLI container needs its in-network database URLs (postgres:5432), and .env contains host-local localhost:5432 URLs for host tools. If you are already inside the interactive just cli shell, set only the provider secret before using the env-var form:

Terminal window
export OPENAI_API_KEY='sk-...'
nexus credentials provider-keys create \
--org "$ORG_SLUG" --project "$PROJECT_SLUG" \
--provider openai --name primary \
--secret-env OPENAI_API_KEY

The secret is wrapped via the local KMS (AES-256-GCM with the key in /var/lib/nexus/control-plane.kms.key inside the compose volume) before it reaches provider_keys.key_ciphertext in Postgres.

Registry sync resolves its upstream credential from the tenant-scope platform key for each provider, so seed platform keys first, then sync:

Terminal window
just load-provider-keys
just models-sync
openai/gpt-5.4-mini
just cli registry models list
just cli registry models get gpt-5.4-mini
# openai/gpt-5.4-mini gpt-5.4-mini https://api.openai.com/v1 0.00000075 0.00000450 enabled

registry models get takes the provider-native model id (gpt-5.4-mini) as a positional argument and prints the Nexus endpoint key (openai/gpt-5.4-mini) in the first column.

nexus registry models endpoints upsert ... adds or adjusts rows without rebuilding the Rust services. The running gateway accepts newly synced models after its registry poll cycle.

just dev already runs nexus-gateway on 0.0.0.0:14450 inside compose and publishes that port to the host. Wait until the gateway container has finished its first cargo build, then check liveness:

Terminal window
curl -fsS http://127.0.0.1:14450/healthz
# ok

If you are not using compose and instead followed the host-binary path from Step 0, start nexus ops serve --bind 127.0.0.1:14450 yourself and use the same curl.

Terminal window
curl -sS http://127.0.0.1:14450/v1/chat/completions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-5.4-mini",
"messages": [{"role": "user", "content": "Say hello in one word."}],
"max_tokens": 8
}' | jq .

You should get back a normal OpenAI chat-completions JSON response. The gateway adds an x-nexus-request-id header to every response — that ID also appears in the gateway.request.completed audit event.

Streaming uses the same endpoint with stream: true:

Terminal window
curl -N http://127.0.0.1:14450/v1/chat/completions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-5.4-mini",
"messages": [{"role": "user", "content": "Say hello in one word."}],
"max_tokens": 8,
"stream": true
}'

The response content type is text/event-stream; charset=utf-8. The gateway forwards the provider stream and records usage from the final stream chunk when the provider includes it.

The CLI mutations write into the audit_events_staging table in nexus_control. The gateway also appends JSONL audit events to /var/lib/nexus/audit.log inside the compose volume; from the repo root, just logs-audit 20 tails that file via the gateway container. Expect five events in order:

  1. org.created
  2. project.created
  3. api_key.created
  4. provider_key.created
  5. gateway.request.completed

The fifth event has attrs.cost_total_usd, attrs.prompt_tokens, attrs.completion_tokens, and attrs.endpoint_key populated from the upstream response.

Stop just dev if it is still running (just down). To wipe databases (compose Postgres):

Terminal window
docker compose --env-file .env -f deploy/compose/docker-compose.yml -f deploy/compose/docker-compose.dev.yml exec postgres \
psql -U vortex -c 'DROP DATABASE nexus_control;'
docker compose --env-file .env -f deploy/compose/docker-compose.yml -f deploy/compose/docker-compose.dev.yml exec postgres \
psql -U vortex -c 'DROP DATABASE nexus_auth;'

Remove the nexus-state volume (and other dev data) with just dev-clean, or keep volumes and only drop the databases above.

If you used a host-only CLI with .nexus/ on disk, also rm -rf .nexus/.

The manual walkthrough above is automated by test-suite/adopter/first-adopter-e2e.sh, runnable as just test-adopter-e2e. It creates a workspace, an org under it (orgs create --workspace), a project, and an API key; extends the org’s classification vocabulary with an org-level label and ceiling; registers a BYOK key; sends non-streaming and streaming chat completions with a session header (including a labeled request, a 400 for a label outside the vocabulary, and a 403 for a label above a tightened project ceiling); confirms the request id and audit evidence; verifies an audit inclusion proof when nexus-audit-verify is available; then revokes the API key and asserts a subsequent request is rejected with 401.

Requirements: a running stack (just dev or the server compose stack with TLS) and OPENAI_API_KEY exported. Point it at a TLS endpoint with NEXUS_BASE_URL=https://host (add INSECURE=1 for self-signed certs).

The managed-PTB extension, first-adopter-ptb-e2e.sh (just test-adopter-ptb-e2e), proves a project with no BYOK key completes a chat via a pass-through-billing attempt with auth_type=ptb. It is gated on NEXUS_GATEWAY_PTB=1 and a platform credential and skips cleanly otherwise.

  • 401 Unauthorized from the gateway. The Authorization header value must be Bearer <token> exactly. Confirm the prefix matches a row in api_keys.prefix in nexus_control.
  • 502 with "no provider key". No provider_keys row matches (org_id, project_id, "openai"). Re-run step 4.
  • gpt-5.4-mini rejected as unknown model. Check just cli registry models list; if the model is missing, re-run just models-sync.
  • nexus ops serve aborts with “recorder already installed”. A different process already holds the global Prometheus recorder in this terminal session. Restart the shell.