API tokens allow headless and non-browser clients (scripts, voice assistants, IoT devices, smart watches) to authenticate independently of the OAuth2 cookie flow. Tokens are long-lived, user-scoped, and grant the same access as the owning user.
When NAVI_AUTH_ENABLED=false, API-token auth is effectively unused because every request already has full admin access. Tokens can still be created, but they all belong to the fixed anonymous user and offer no additional isolation.
nav_<43-char URL-safe string> — e.g. nav_aB3xYz9WqLmNpQrStUvXyZaBX-Api-Token HTTP header for REST; ?api_token= query parameter for WebSocketrevoked_at timestamp). Revoked tokens immediately return 401.┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Headless │────▶│ X-Api-Token │────▶│ navi_users │ │ client │ │ header or │ │ + api_tokens│ │ │◀────│ ?api_token │◀────│ (PostgreSQL)│ └──────────────┘ └──────────────┘ └──────────────┘
Token resolution flow (navi/auth/deps.py::_resolve_user_from_api_token):
X-Api-Token header or api_token query paramapi_tokens JOIN navi_usersrevoked_at IS NULLUser from navi_users rowUPDATE api_tokens SET last_used_at = NOW()CREATE TABLE api_tokens (
id SERIAL PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES navi_users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
token_prefix TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
last_used_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ
);
| Column | Description |
|---|---|
token_hash |
SHA-256 of the plain token. Never stores plain text. |
token_prefix |
First 12 chars of the token (nav_aB3xYz9W…) for UI identification. |
revoked_at |
Soft-delete marker. NULL = active. |
POST /api-tokensCreate a new API token. Plain token returned only in this response.
Auth: requires authenticated user.
Request
{ "name": "Smart Watch" }
Response 200
{
"id": 1,
"name": "Smart Watch",
"token": "nav_aB3xYz9WqLmNpQrStUvXyZaBCdEfGhIjKlMnOpQrStUvX",
"token_prefix": "nav_aB3xYz9W…",
"created_at": "2026-05-24T10:00:00+00:00",
"last_used_at": null
}
GET /api-tokensList active (non-revoked) tokens for the current user. Does not expose plain tokens.
Auth: requires authenticated user.
Response 200
{
"items": [
{
"id": 1,
"name": "Smart Watch",
"token_prefix": "nav_aB3xYz9W…",
"created_at": "2026-05-24T10:00:00+00:00",
"last_used_at": "2026-05-24T12:00:00+00:00"
}
]
}
DELETE /api-tokens/{token_id}Revoke (soft-delete) a token belonging to the current user.
Auth: requires authenticated user.
Response 204 — no body
Errors
404 — token not found or does not belong to usercurl -H "X-Api-Token: nav_aB3xYz9WqLmNpQrStUvXyZaBCdEfGhIjKlMnOpQrStUvX" \ https://navi.gnexus.space/api/agents/profiles
Append the token as a query parameter:
const ws = new WebSocket(
`wss://navi.gnexus.space/ws/sessions/${sessionId}?api_token=${encodeURIComponent(token)}`
)
Security note: The query parameter is visible in server access logs. For production deployments with strict log retention policies, consider implementing a
{type: "auth", api_token: "..."}WebSocket message sent immediately after connect.
The web client stores the token in localStorage under key navi_api_token. When present, the WebSocket composable automatically appends ?api_token= on connect.
| Risk | Mitigation | Status |
|---|---|---|
| Token in URL query param → server logs | Documented; future: auth WS message | Accepted |
Token in localStorage → XSS exposure |
Single-origin policy; no third-party JS | Accepted |
| No rate limiting on token creation | Limited by requiring authenticated user | Accepted |
| No per-token scopes or expiration | MVP simplification; schema supports adding later | Accepted |
| Token hash collision | SHA-256 with 256-bit random input — negligible | Mitigated |
| Brute-force token guessing | 43-char URL-safe = ~256 bits; computationally infeasible | Mitigated |
The web client exposes token management at #settings (Settings → API Keys):