Server Administration Guide (multi-user nova server)

This guide is for the administrator of a shared, multi-user NovaFabric deployment — the nova server REST API (/v0), not the single-operator nova serve dashboard. It covers identity, roles, tenancy, and audit administration. For installation and deployment topologies see the Server Deployment Guide; this guide does not repeat it.

Everything below is labelled per the docs honesty rule: works today, experimental, planned, or future design.

The two servers, in one line: nova server is the team/API surface (OIDC, RBAC, /v0); nova serve is the localhost dashboard (single shared token). See docs/api-reference.md for the full disambiguation table.


1. Enabling server mode

Status: experimental (v0.7+, ADR-0017, ADR-0029).

pip install 'novafabric[server]'
nova server start                       # 127.0.0.1:7433 by default
nova server start --host 0.0.0.0 --port 8080

Configuration lives in ~/.config/novafabric/server.yaml; only --config <path> points the server at another file (for example /etc/novafabric/server.yaml). NOVAFABRIC_SERVER_* environment variables override individual settings, and are held to the same bounds as the YAML keys: an out-of-range value (NOVAFABRIC_SERVER_PORT=99999, NOVAFABRIC_SERVER_INGEST_MAX_CONCURRENT_UNPACK=0) makes nova server start exit 2 with Invalid server config, naming the field. Secrets are environment-only and stripped from YAML: NOVAFABRIC_POSTGRES_DSN, NOVAFABRIC_OFFLINE_KEY_PATH, NOVAFABRIC_SCIM_TOKEN and NOVAFABRIC_SERVER_TOKEN (server/config.py::load_config). See Server Deployment Guide — Scenario 2 for the Postgres backend and migration from SQLite.

Local auth (ADR-0184, experimental — shipped 2026-07-16). With OIDC disabled, the server requires an auto-generated local bearer token (printed at startup, stored at $NOVAFABRIC_HOME/.server-token mode 0600, pinnable via NOVAFABRIC_SERVER_TOKEN). The old anonymous-admin behavior needs the explicit --insecure-no-auth opt-out and refuses non-loopback binds without --i-know-this-is-public (ADR-0184). Each insecure start logs a warning and appends a server.insecure_no_auth entry (bind host, port, loopback, public-bind confirmation) to the hash-chained audit log at NOVAFABRIC_AUDIT_LOG_PATH (default $XDG_DATA_HOME/novafabric/audit.jsonl, else ~/.local/share/novafabric/audit.jsonl; NOVAFABRIC_HOME does not move it — set the variable explicitly to put the log on a data volume). If the entry cannot be written the server refuses to start in insecure mode.

2. Identity backends

Backend Status Notes
OIDC (JWT Bearer + JWKS) experimental The primary team backend. Setup: Server Deployment Guide — Scenario 3. JWKS cache flush: nova server flush-jwks-cache. ADR-0018
Offline ed25519 tokens experimental Air-gapped/CI/SLURM machine identity without an IdP. nova server issue-token --subject worker-01 --roles reader,writer --expires-in 30d; revoke with nova server revoke-token <jti>. Revocations are recorded in the token_audit table.
SCIM 2.0 provisioning experimental Off by default (endpoints 404). Enable with NOVAFABRIC_SERVER_SCIM_ENABLED=1 and NOVAFABRIC_SCIM_TOKEN. /scim/v2/* per RFC 7644; all provisioning actions land in the append-only scim_audit_events table. ADR-0139
SAML 2.0 SSO experimental, opt-in SP metadata via nova server saml-metadata. POST /v0/auth/saml/acs consumes assertions only when saml.experimental_acs_enabled: true is set in server.yaml and novafabric[saml] (signxml + lxml, cleared the license gate in v0.73.0) is installed; otherwise it returns 501. Even then a verified assertion is answered 501 saml_session_unavailable: the server cannot yet issue a session for a SAML login. NovaFabric never skips assertion signature verification. A Security-Architect review remains a pre-production blocking condition (ADR-0138 §D5).
Device-grant demo flow off by default (endpoints 404) The RFC 8628 /v0/auth/device/code|token|approve flow is local/testing scaffolding whose HS256 tokens the real verifier never honours; /approve is unauthenticated. It stays unmounted unless you set NOVAFABRIC_SERVER_DEMO_DEVICE_GRANT=1. Never enable it in production — use OIDC or offline tokens instead. ADR-0198

3. Roles and authorization

Status: experimental.

ADR-0018 defines six built-in roles. Four are enforced as route-level checks:

Role Grants
reader Read access to runs, capsules, lineage
writer reader + create/ingest
admin Everything, including role management
auditor Orthogonal — audit trails and evidence only; cannot write

The hierarchy is reader < writer < admin; admin satisfies every check; auditor satisfies only auditor checks. The remaining two roles — promoter and approver — implement separation of duties in the maker-checker promotion and NovaSeal approval flows (ADR-0058); the same identity can never serve as both maker and checker for one proposal.

Assigning roles (either surface writes the same role_assignments table):

nova server assign-role alice@example.com writer
# or over HTTP (admin-gated):
#   GET/POST /v0/admin/roles, DELETE /v0/admin/roles/{subject}/{role}

⚠ A role_assignments row is consulted only once the workspace/org model is in use (a scoped membership or service account exists). In a stock deployment it grants nothing: assign-role then warns this assignment is not in force on stderr and GET /v0/admin/roles reports effective_now: false. Carry the role in the token instead (OIDC nova_roles/roles claim, or issue-token --roles).

Experimental: org/workspace-scoped role bindings and named service accounts (/v0/orgs, /v0/workspaces, /v0/service-accounts) — ADR-0178. Creating the first membership or service account is what switches role_assignments rows on.

What a binding grants, when a credential's own roles fall short:

4. Tenancy and isolation

Status: experimental (Postgres backend only).

With the Postgres metadata store, tenant isolation is enforced in the database, not in application code: FORCE ROW LEVEL SECURITY with a tenant_isolation policy on runs, capsules, signatures, and retention_policies, a per-transaction SET LOCAL app.current_tenant_id (safe under pgBouncer transaction pooling), and a split between the novafabric_app role (no BYPASSRLS) and novafabric_migrator (ADR-0040, ADR-0052). The policy holds only when the server connects as novafabric_app (or another role without BYPASSRLS). A Postgres superuser bypasses row-level security even under FORCE, so never point NOVAFABRIC_METADATA_DSN at a superuser. The app role is granted SELECT, INSERT, UPDATE on the tenant tables. Capsule deletion (DELETE /v0/capsules/{run_id}, POST /v0/capsules/bulk-delete) also needs DELETE on runs, capsules and signatures, which migration v005 grants — run nova db upgrade --backend postgres after upgrading. A CI gate (metadata_store_security_gate) re-proves cross-tenant isolation on every change.

Tenancy today is a single flat tenant_id per deployment-defined scope. There is no organization/workspace/team hierarchy yet — that is ADR-0178 (future design), which keeps tenant_id as the sole RLS key.

5. Audit trails

Status: works today (local + server).

6. API behavior an admin should know

6a. Bulk capsule operations + keyset pagination (experimental, ADR-0206)

Status: experimental — shipped by ADR-0206. The normative contract is specified in the maintainers' private design/ tree and is not published.

Keyset pagination on GET /v0/capsules:

Keyset pagination on GET /v0/assets (experimental, ADR-0206 P2) — same cursor format and rules as above: order created_at DESC, id DESC, seek in the registry SQL (spec_json is never read for a list page), total on the first page only, 400 invalid_cursor on garbage (previously a silent restart at page one), legacy {"offset": N} cursors served with Deprecation: true until legacy_offset_cursors: false. Cursors are not bound to the asset_type/status filters — reuse a cursor only with the filters it was issued under.

Keyset pagination on GET /v0/lineage/nodes (experimental, ADR-0206 P2) — order node_id ASC (the primary key, so the seek needs no new index), the same v1 cursor with k = [null, node_id]; a cursor from another listing is a 400 invalid_cursor. total on the first page only (and on legacy pages), legacy {"offset": N} cursors served with Deprecation: true until legacy_offset_cursors: false. Behaviour change: items used to come back in insertion order and a garbage cursor silently restarted at page one. Cursors are not bound to the kind filter — reuse a cursor only with the filter it was issued under. With kind=, SQLite walks the primary key and filters; a (kind, node_id) index would make that seek O(page) (planned — registry schema change).

Governed deletion (admin role only — writer/reader/auditor get 403):

7. Backup and restore

See the dedicated Backup & Restore Runbook — manual procedures that work today, the nova backup tooling (ADR-0181, experimental), and the automated Postgres restore (nova restore — pg profile auto-detected, ADR-0217, ADR-0211, experimental).

7a. Startup schema-skew guard (experimental, ADR-0211)

Before the server touches its database at startup, the lifespan compares the DB's Alembic stamp against the installed build's migration head (registry track; both sqlite and postgres backends):

Break-glass: NOVAFABRIC_ALLOW_SCHEMA_SKEW=1 downgrades refusals to a structured warning (event=schema_skew_overridden) and starts — for emergency read-mostly access only; unset after the incident. Refusal happens before init_schema() and the org bootstrap, so a refused server mutates nothing. Error/warning text carries backend name and revisions only — never DSNs or hostnames. /readyz's migrations check uses the same comparator and reports real ok/fail for Postgres too.

8. Enterprise hardening at a glance

First slices shipped experimental 2026-07-16; tracked in the enterprise-readiness plan:

Feature ADR Status
Secure-by-default local auth (no anonymous admin) 0184 experimental
Workspaces, organizations, service accounts (/v0/orgs, /v0/workspaces, /v0/service-accounts) 0178 experimental
Rate limiting + storage quotas (default off; quota enforcement is warn-then-reject) 0179 experimental
/metrics, /livez, /readyz, /v0/version 0182 experimental
Support bundle (nova support-bundle) 0187 experimental
Backup sets (nova backup create/verify, nova restore local profile) 0181 experimental
Automated pg restore (nova restore, manifest-driven — 0217) + startup schema-skew guard (0211 Part B) 0217 / 0211 experimental

9. Webhook subscriptions (experimental)

Status: experimental (ADR-0205, spec webhook-registry-v0.md) — API shapes may change. Server mode only; the env-configured NOVA_EVENTS_* / NOVA_ALERTS_* sinks are unchanged.

API-managed outbound event subscriptions: /v0/webhooks CRUD + test ping, a persisted per-attempt delivery log with explicit redelivery, and HMAC-signed delivery through the ADR-0137 sink core.

# ~/.config/novafabric/server.yaml
webhooks:
  enabled: true                  # default false — off ⇒ no dispatch worker at all
  queue_max: 1000                # bounded dispatch queue (overflow = drop-with-audit)
  max_attempts: 5                # POSTs per delivery chain (1–10); backoff 0s/30s/2m/10m/1h
  timeout_s: 5.0                 # per-POST timeout
  delivery_retention_days: 30    # delivery-log age cap
  delivery_retention_rows: 10000 # delivery-log per-webhook row cap
  allow_insecure_url: false      # permit non-loopback http:// endpoints (audited opt-out)
  allow_internal_targets: false  # v0.98.0 SSRF guard opt-out — see below

Env overrides follow NOVAFABRIC_SERVER_WEBHOOKS_* (plus the spec's NOVAFABRIC_WEBHOOKS_QUEUE_MAX alias for the queue bound).

Operating notes: