omnigraph/docs/user/server.md
andrew 0685d5530f omnigraph-server: optional CORS layer for browser-based UIs
Off by default — production deployments behind a same-origin reverse
proxy need no configuration. When OMNIGRAPH_SERVER_CORS_ORIGIN is set
(comma-separated origins), attach a tower_http::cors::CorsLayer
permitting GET/POST/DELETE plus Authorization and Content-Type request
headers. Empty/unset variable preserves prior no-CORS behavior.

Tests: cors_default_off_does_not_emit_allow_origin_header,
cors_env_origin_emits_allow_origin_header.

Companion to omnigraph-ui's web demo (Vite dev origin
http://127.0.0.1:5173 hitting the API at 127.0.0.1:8080).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:00:46 +03:00

5.6 KiB

HTTP Server (omnigraph-server)

Axum 0.8 + tokio + utoipa-generated OpenAPI. Single repo per process; deploy multiple processes for multi-tenant.

Endpoint inventory

Method Path Auth Action Handler
GET /healthz none server_health
GET /openapi.json none server_openapi (strips security if auth disabled)
GET /snapshot?branch= bearer + read snapshot of branch server_snapshot
POST /read bearer + read run named query server_read
POST /export bearer + export NDJSON stream server_export
POST /change bearer + change mutation server_change
GET /schema bearer + read get current .pg source server_schema_get
POST /schema/apply bearer + schema_apply (target=main) migrate server_schema_apply
POST /ingest bearer + branch_create (if new) + change bulk load server_ingest (32 MB body limit)
GET /branches bearer + read list branches server_branch_list
POST /branches bearer + branch_create create server_branch_create
DELETE /branches/{branch} bearer + branch_delete delete server_branch_delete
POST /branches/merge bearer + branch_merge merge source → target server_branch_merge
GET /commits?branch= bearer + read list server_commit_list
GET /commits/{commit_id} bearer + read show server_commit_show

Streaming

Only /export streams (application/x-ndjson, MPSC channel + Body::from_stream). Everything else is buffered JSON.

Error model

Uniform ErrorOutput { error, code?, merge_conflicts[], manifest_conflict? } with code ∈ unauthorized | forbidden | bad_request | not_found | conflict | too_many_requests | internal. Merge conflicts attach structured MergeConflictOutput { table_key, row_id?, kind, message }.

manifest_conflict is set on publisher CAS rejections (HTTP 409): the caller's pre-write view of one table's manifest version was stale. ManifestConflictOutput { table_key, expected, actual } tells the client which table to refresh and retry. This is the conflict shape produced by concurrent /change or /ingest calls landing the same (table, branch) race.

HTTP status codes used: 200, 400, 401, 403, 404, 409, 429, 500.

Per-actor admission control

Disjoint (table, branch) writes from different actors now run concurrently, guarded only by the engine's per-(table, branch) write queue. To keep one heavy actor from exhausting shared capacity (Lance I/O, manifest churn, network), the server gates mutating handlers through a WorkloadController configured per-process from environment variables:

Env var Default Purpose
OMNIGRAPH_PER_ACTOR_INFLIGHT_MAX 16 Concurrent in-flight mutations per actor
OMNIGRAPH_PER_ACTOR_BYTES_MAX 4 GiB In-flight estimated bytes per actor

When an actor exceeds its in-flight count or byte budget, the server returns HTTP 429 Too Many Requests with code: too_many_requests and a Retry-After header (seconds). The actor should back off; other actors are unaffected.

Cedar policy authorization runs before admission accounting so denied requests don't consume admission slots.

Today admission gates every mutating handler: /change, /ingest, /branches/{create,delete,merge}, and /schema/apply. Read-only endpoints (/snapshot, /read, /export, /branches GET, /commits, /schema GET) are not admission-gated.

Body limits

  • Default: 1 MB
  • /ingest: 32 MB

Auth model (bearer + SHA-256)

  • Tokens are SHA-256 hashed on startup; plaintext is never persisted in memory.
  • Constant-time comparison via subtle::ConstantTimeEq.
  • Three sources, in precedence:
    1. OMNIGRAPH_SERVER_BEARER_TOKENS_AWS_SECRET — AWS Secrets Manager (build with --features aws)
    2. OMNIGRAPH_SERVER_BEARER_TOKENS_FILE or OMNIGRAPH_SERVER_BEARER_TOKENS_JSON — JSON {actor_id: token, …}
    3. OMNIGRAPH_SERVER_BEARER_TOKEN — single legacy token, actor default
  • If no tokens configured, server runs unauthenticated (local dev) and /openapi.json strips the security scheme.

See deployment.md for token-source operational details.

Tracing & observability

  • tower_http::TraceLayer::new_for_http()
  • Policy decisions logged at INFO level with actor, action, branch, decision, matched rule
  • Startup logs: token source name, repo URI, bind address
  • Graceful SIGINT shutdown

CORS

Off by default — production deployments behind a same-origin reverse proxy need no configuration. To enable cross-origin requests (e.g. from a browser-based UI on a different host/port during development), set:

  • OMNIGRAPH_SERVER_CORS_ORIGIN — comma-separated list of allowed origins. Example: OMNIGRAPH_SERVER_CORS_ORIGIN=http://localhost:5173,https://app.example.com.

When set, the server attaches a tower_http::cors::CorsLayer permitting GET, POST, DELETE, plus the Authorization and Content-Type request headers. Requests from origins not in the list receive no CORS allow-origin response and the browser blocks them. Empty/unset variable → no layer → no CORS headers (default behaviour preserved).

Not implemented (by design or "TBD")

  • Rate limiting — per-actor admission control gates /change, /ingest, /branches/{create,delete,merge}, /schema/apply (see "Per-actor admission control" above). No global rate limiter is configured; add tower_http::limit if a graph-wide cap is needed.
  • Pagination — none (commits/branches return everything; export streams).
  • Multi-tenant routing — one repo per process.