Skip to main content

Quick start

Prerequisites

  • Docker with the Compose plugin (docker compose)
  • A clone of the qwed-verification repository
Compose builds the API server from the repository’s Dockerfile (the server target) and waits for Postgres, Redis, and Jaeger to report healthy before it starts the API.
Compose refuses to start while QWED_CORS_ORIGINS, API_KEY_SECRET, QWED_JWT_SECRET_KEY, QWED_API_KEY_LOOKUP_SECRET, QWED_AUDIT_SECRET_KEY, or DATABASE_URL is empty. The Postgres container refuses to start while POSTGRES_PASSWORD is empty, and the Grafana container refuses to start while GF_SECURITY_ADMIN_PASSWORD is empty.

Services

The API server keeps its data in the qwed-data volume. Postgres, Redis, Prometheus, and Grafana each have their own named volume, so data survives docker compose down.

Configuration

Environment variables

deploy/.env.example lists every setting the Compose stack reads. Generate each secret separately:
Postgres and Grafana receive only their own settings, not the API server’s secrets. deploy/.env is git-ignored.
QWED_METRICS_OPERATOR_USER_IDS is optional. Leave it empty and every caller, including the Prometheus scraper, gets denied on /metrics. Prometheus records 401 responses until you configure an operator. See Prometheus metrics. See the deployment guide for the full variable reference.

API configuration

Docker

API server image

The qwedai/qwed-verification image on Docker Hub runs the API server with uvicorn on port 8000. Docker Hub publishes latest from main and a versioned tag for each release from 8.0.0. The image:
  • Runs as a non-root user (UID 1000).
  • Uses /var/lib/qwed as its working directory. The default SQLite database (sqlite:///./qwed.db) lives there. Mount a volume at /var/lib/qwed to keep it, or set DATABASE_URL to use Postgres.
  • Has a health check that calls GET /health with the image’s own Python.
  • Reads its bind address and port from UVICORN_HOST (default 0.0.0.0) and UVICORN_PORT (default 8000). The health check follows UVICORN_PORT, so change the port with -e UVICORN_PORT=... rather than a --port argument.
Tags before 8.0.0 ran the QWED GitHub Action, not the API server. If you ran the image as a repository scanner, use the GitHub Action instead.

Run the published image

Pass the secrets from your shell, so no value is written into the command:
The server refuses to start without any of QWED_CORS_ORIGINS, API_KEY_SECRET, QWED_JWT_SECRET_KEY, QWED_API_KEY_LOOKUP_SECRET and QWED_AUDIT_SECRET_KEY, and QWED_API_KEY_LOOKUP_SECRET must differ from QWED_JWT_SECRET_KEY. Also set QWED_SIGNING_KEY (an EC P-256 key), or attestations stop verifying after a restart or on another replica. The server names no model. For requests that use a provider (natural-language, logic, statistics, and fact), also pass the provider’s settings, for example -e ACTIVE_PROVIDER -e OPENAI_API_KEY -e OPENAI_MODEL. The container doesn’t read the qwed init settings on your host. If you keep the values in a .env file instead, pass it with --env-file .env. Optionally, add a signing key so attestations verify after a restart and on every replica. Without one, each process generates its own key. Mount an unencrypted EC P-256 PEM file and point QWED_SIGNING_KEY_FILE at it by adding these flags to docker run:
See Signing keys and the environment variables reference. To serve on a different port, with the same secrets exported:

Build from source

The repository’s Dockerfile has two targets. server is the API server. action is the GitHub Action and stays the default target, so build the server with --target server:

Kubernetes

The repository ships manifests in deploy/kubernetes/. The Deployment pins docker.io/qwedai/qwed-verification:9.1.0. Tags from 8.0.0 contain the API server. Fill in every value in deploy/kubernetes/secret.yaml before you apply it. See Kubernetes deployment.
🏢 Enterprise Support Coming Soon: Managed hosting, dedicated support, and SLA guarantees. Contact support@qwedai.tech

Scaling

Horizontal scaling

  • Stateless API servers behind load balancer
  • Redis for distributed rate limiting
  • PostgreSQL with read replicas

Performance tuning

Run one uvicorn worker per database. Multiple workers or replicas writing audit rows to the same database are not supported. Every verification request appends an audit row, and the audit logger keeps each organization’s chain head in memory. With --workers 4 or several replicas, a conflicting append makes the verification request fail with 500 audit persistence failed instead of forking the chain. See Chain continuity across restarts.

Availability limits

QWED bounds request size, slow work, and sandbox usage so one request can’t stall or exhaust a server process. The defaults suit most deployments. Raise them only if your host has the headroom.
Request body caps are fixed. Every /verify JSON route accepts up to 1,000,000 bytes. /verify/stats and /verify/image accept their 10 MB file limit plus envelope headroom. QWED rejects an oversized body with 413 before processing it. See Request limits for the full error list.
QWED_SANDBOX_MAX_CONCURRENT applies per worker process. With uvicorn --workers 4 and the default of 4, a host can run up to 16 sandbox containers. Size the value against your host’s memory, CPU, and PID limits.

Sandbox image and container user

v8.0.0
/verify/stats and /verify/consensus run model-generated Python in a Docker sandbox. QWED pins the sandbox image by digest, so the sandbox only ever runs the exact image that was reviewed:
Each sandbox container runs with a read-only root filesystem, drops all Linux capabilities, sets no-new-privileges, and never runs as a privileged root. The only writable path is the per-execution /workspace directory, which is a private 0700 host directory. The container runs as that directory’s owner:
  • Non-root server process: the container runs as the host user.
  • Root server process: QWED hands each workspace to a fresh uid for that execution. QWED skips any uid that a host account or group owns, that /etc/subuid or /etc/subgid delegates, or that another running execution uses.
  • Rootless daemon run by the same non-root user: the container runs as its root, which the daemon maps to that unprivileged host user.
QWED never guesses how the Docker daemon maps users. It reports the sandbox as unavailable (SECURE_RUNTIME_UNAVAILABLE) when any of the following is true. /verify/stats then returns BLOCKED with constraint stats_verifier.runtime_unavailable, and /verify/consensus counts its code-execution engine as BLOCKED.
  • the daemon uses userns-remap
  • the server process runs as root with a rootless daemon
  • QWED can’t read the daemon’s security options

Monitoring

Health check

Prometheus metrics

Scrape GET /metrics/prometheus (the Prometheus text exposition format — /metrics returns JSON). The endpoint requires a platform operator: create a dedicated scraper account, add its user ID to QWED_METRICS_OPERATOR_USER_IDS (fail-closed when unset — every caller is denied, including org owners/admins), mint the account an API key, and send it as a header on the scrape. See All-tenant metrics authorization.

Security

  1. Always use HTTPS in production
  2. Set API_KEY_SECRET — mandatory, no default value
  3. Set QWED_CORS_ORIGINS — mandatory, explicitly list allowed origins
  4. Set strong API keys
  5. Enable rate limiting
  6. Use network isolation
  7. Rotate secrets regularly
  8. Configure QWED_METRICS_OPERATOR_USER_IDS if anything needs all-tenant metrics — unset means deny-all