Quickstart

Deploy the supported Docker Compose stack, wire up a provider and model, issue an API key, and make your first request.

This guide takes you from nothing to a working request against a self-hosted Cordy Gateway. It uses the supported deployment path — Docker Compose — and ends with a real chat completion via curl, the OpenAI Python SDK, and the OpenAI JavaScript SDK.

Plan for about 15–20 minutes. You will:

  1. Check requirements.
  2. Configure secrets in a .env file.
  3. Bring up the production stack with Docker Compose.
  4. Create an admin user.
  5. Register a provider, channel, and model in the Admin.
  6. Issue a wnx_ API key.
  7. Make your first request.
  8. Verify health.

1. Requirements

  • Docker Engine 24+ and the Docker Compose v2 plugin (docker compose, not the legacy docker-compose).
  • The Cordy Gateway distribution (the repository or delivery bundle) on the host, so that docker-compose.prod.yml and the deploy/ and docker/ directories are present.
  • A host with at least 4 GB RAM free for the full stack (Postgres, Redis, PgBouncer, Admin, Gateway, worker, nginx).
  • For a real production deployment: a domain name and TLS certificate. The bundled nginx terminates plain HTTP; put a 443 server block and certs in docker/nginx/, or front the stack with your own reverse proxy. For a local trial you can skip TLS (see the note in step 2).

Verify Docker is ready:

docker --version
docker compose version

2. Configure secrets

Copy the template and fill in every placeholder:

cp deploy/env.template .env

Generate the three secrets you must set:

# Django secret key
python -c "import secrets; print(secrets.token_urlsafe(50))"

# Channel credential encryption key (Fernet)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

# A strong database password — any long random string
python -c "import secrets; print(secrets.token_urlsafe(24))"

Set at least these required variables in .env:

VariablePurposeNotes
DB_NAMEPostgres database namee.g. cordy
DB_USERPostgres user
DB_PASSWORDPostgres passwordUse the generated value
DJANGO_SECRET_KEYDjango cryptographic keyUse the generated value
DJANGO_ALLOWED_HOSTSHostnames the Admin servesComma-separated; add localhost,127.0.0.1 for a local trial
DJANGO_ADMIN_URLAdmin URL path prefixMust end with /; pick a non-obvious value, e.g. ops-7f3a/
CHANNEL_ENCRYPTION_KEYSFernet key(s) for upstream credentialsMust be identical for the Admin and Gateway services

Local-trial note: the bundled nginx serves plain HTTP. For a no-TLS local test only, set DJANGO_SECURE_SSL_REDIRECT=False in .env so the Admin does not force an HTTPS redirect. Leave it True (the default) for any real deployment.

CHANNEL_ENCRYPTION_KEYS is mandatory in production: the Gateway and Admin both refuse to start without it, and both must use the same value or channel credentials cannot be decrypted. Keep .env out of version control and store secrets in your secrets manager.

See the Configuration reference for every variable.

3. Bring up the stack

docker compose -f docker-compose.prod.yml --env-file .env up -d --build

This starts the full production topology:

ServiceRolePort
nginxLoad balancer / TLS termination pointPublishes GATEWAY_HTTP_PORT (default 8002)
gatewayData plane (Starlette + Granian, OpenAI-compatible API)Internal 8002, fronted by nginx
adminControl plane (Django Admin)Publishes ADMIN_HTTP_PORT (default 8001)
postgresPostgreSQL 18 with pg_cronInternal only
pgbouncerTransaction connection pooler for the gateway hot pathInternal only
redisRedis 8 — rate limits, quotas, caching, buffersInternal only
workerBackground worker (usage settlement, audit upload)Internal only

The Admin container automatically runs database migrations and collects static files before starting. Postgres, PgBouncer, and Redis are internal-only and are not published to the host.

Watch startup and confirm the services are healthy:

docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs -f admin

4. Create an admin user

Create a Django superuser to sign in to the Admin:

docker compose -f docker-compose.prod.yml exec admin \
  uv run python manage.py createsuperuser

Follow the prompts for email and password. Then sign in at:

http://<host>:8001/<DJANGO_ADMIN_URL>

For example, if DJANGO_ADMIN_URL=ops-7f3a/, the login URL is http://<host>:8001/ops-7f3a/.

5. Register a provider, channel, and model

Cordy Gateway resolves the model a client requests through a chain of records. You create them once in the Admin under Gateway Configuration. The minimum wiring to serve one model:

  1. Provider — the upstream vendor integration (for example, OpenAI). Set name, provider_type (e.g. OPENAI), and status ACTIVE.
  2. Public model — the public identifier clients will request in the model field (for example, a slug gpt-4o-mini). This is the only model name your clients ever see.
  3. Provider model — links the Provider and the Public model to the concrete upstream model name (for example, gpt-4o-mini), with input_price / output_price and capability flags (supports_chat, supports_embeddings, supports_streaming).
  4. Channel — a routing target on the Provider. Set its upstream credential (the API key — stored Fernet-encrypted), optional base_url, weight, priority, and optional region.
  5. Channel provider model — binds the Channel to the Provider model, enabling that model to be served through that channel. The provider model must belong to the channel's provider.

Once at least one active channel can serve your public model, the model becomes routable. See Providers for a full walkthrough and Concepts for how the chain resolves at request time.

6. Issue an API key

In the Admin, create (or pick) a user, then create an API key for that user. On creation, the gateway generates a wnx_-prefixed key and shows the full plaintext exactly once — copy it immediately; only its SHA-256 hash is stored.

When creating the key, set:

  • Scopes — the endpoint families the key may call (chat, completions, embeddings, models, balance, admin). Grant only what the key needs; an empty scope list means "allow all endpoints the owner can otherwise reach" (legacy default). New keys should be least-privilege.
  • Quota — monthly token allowance.
  • Rate limit — requests per minute.

Export the key for the next step:

export CORDY_API_KEY="wnx_...your key..."
export CORDY_BASE_URL="http://<host>:8002/v1"   # use https://... in production

7. Make your first request

curl

curl -sS "$CORDY_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $CORDY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o-mini",
    "messages": [
      {"role": "user", "content": "Say hello from Cordy Gateway in one sentence."}
    ]
  }'

Replace gpt-4o-mini with the public model slug you created. A successful response is a standard OpenAI chat completion object.

Python (OpenAI SDK)

pip install openai
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["CORDY_API_KEY"],
    base_url=os.environ["CORDY_BASE_URL"],  # http://<host>:8002/v1
)

resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "user", "content": "Say hello from Cordy Gateway in one sentence."}
    ],
)
print(resp.choices[0].message.content)

JavaScript / TypeScript (OpenAI SDK)

npm install openai
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.CORDY_API_KEY,
  baseURL: process.env.CORDY_BASE_URL, // http://<host>:8002/v1
});

const resp = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [
    { role: "user", content: "Say hello from Cordy Gateway in one sentence." },
  ],
});

console.log(resp.choices[0].message.content);

8. Verify health

The health endpoints are public (no API key required):

# Liveness — is the process up?
curl -sS http://<host>:8002/health

# Readiness — can it actually serve chat/embeddings right now?
curl -sS http://<host>:8002/health/ready

# Per-provider health breakdown
curl -sS http://<host>:8002/health/providers

/health returns 200 as soon as the process is running. /health/ready and /health/detailed return 503 until at least one channel can serve traffic — this is expected before you finish the wiring in step 5, and turns to 200 once a model is routable.

Troubleshooting

  • Every request returns 401 right after deploy. The gateway data plane builds its database DSN from DB_NAME / DB_USER / DB_PASSWORD directly (it does not parse DATABASE_URL). If these are missing or wrong on the gateway service, it cannot reach the database and auth fails for every request. Confirm the gateway's DB_* values match the admin's.
  • 401 invalid_api_key. The key is wrong or was never activated. Re-issue the key in the Admin and copy the one-time plaintext exactly, including the wnx_ prefix.
  • 404 model_not_found or an empty /v1/models list. The model chain is incomplete or inactive. Confirm the Public model, Provider model, Channel, and Channel-provider-model records all exist and are ACTIVE, and that the channel has a valid upstream credential.
  • /health/ready stays 503. No channel can serve traffic yet. Finish the provider/channel/model wiring, then check /health/providers for the per-provider status and the admin and gateway logs.
  • Admin login redirects to HTTPS on a local no-TLS trial. Set DJANGO_SECURE_SSL_REDIRECT=False in .env and recreate the admin service. Never do this for a real deployment.
  • CHANNEL_ENCRYPTION_KEYS errors at startup. The variable is unset, or the Admin and Gateway have different values. Set the same Fernet key(s) for both services.
  • Provider calls fail with an upstream auth error. The channel's stored credential is wrong. Edit the channel in the Admin and re-enter the upstream API key.

Next steps

  • Client integration — streaming, tool calling, embeddings, and product headers.
  • API reference — every endpoint, scope, and error code.
  • Deployment — scaling, observability, backups, and upgrades.
  • Security — the trust model and hardening checklist.