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:
- Check requirements.
- Configure secrets in a
.envfile. - Bring up the production stack with Docker Compose.
- Create an admin user.
- Register a provider, channel, and model in the Admin.
- Issue a
wnx_API key. - Make your first request.
- Verify health.
1. Requirements
- Docker Engine 24+ and the Docker Compose v2 plugin (
docker compose, not the legacydocker-compose). - The Cordy Gateway distribution (the repository or delivery bundle) on the host, so that
docker-compose.prod.ymland thedeploy/anddocker/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
443server block and certs indocker/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:
| Variable | Purpose | Notes |
|---|---|---|
DB_NAME | Postgres database name | e.g. cordy |
DB_USER | Postgres user | |
DB_PASSWORD | Postgres password | Use the generated value |
DJANGO_SECRET_KEY | Django cryptographic key | Use the generated value |
DJANGO_ALLOWED_HOSTS | Hostnames the Admin serves | Comma-separated; add localhost,127.0.0.1 for a local trial |
DJANGO_ADMIN_URL | Admin URL path prefix | Must end with /; pick a non-obvious value, e.g. ops-7f3a/ |
CHANNEL_ENCRYPTION_KEYS | Fernet key(s) for upstream credentials | Must 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=Falsein.envso the Admin does not force an HTTPS redirect. Leave itTrue(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:
| Service | Role | Port |
|---|---|---|
nginx | Load balancer / TLS termination point | Publishes GATEWAY_HTTP_PORT (default 8002) |
gateway | Data plane (Starlette + Granian, OpenAI-compatible API) | Internal 8002, fronted by nginx |
admin | Control plane (Django Admin) | Publishes ADMIN_HTTP_PORT (default 8001) |
postgres | PostgreSQL 18 with pg_cron | Internal only |
pgbouncer | Transaction connection pooler for the gateway hot path | Internal only |
redis | Redis 8 — rate limits, quotas, caching, buffers | Internal only |
worker | Background 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:
- Provider — the upstream vendor integration (for example, OpenAI). Set
name,provider_type(e.g.OPENAI), and statusACTIVE. - Public model — the public identifier clients will request in the
modelfield (for example, a sluggpt-4o-mini). This is the only model name your clients ever see. - Provider model — links the Provider and the Public model to the concrete upstream model name (for example,
gpt-4o-mini), withinput_price/output_priceand capability flags (supports_chat,supports_embeddings,supports_streaming). - Channel — a routing target on the Provider. Set its upstream credential (the API key — stored Fernet-encrypted), optional
base_url,weight,priority, and optionalregion. - 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
401right after deploy. The gateway data plane builds its database DSN fromDB_NAME/DB_USER/DB_PASSWORDdirectly (it does not parseDATABASE_URL). If these are missing or wrong on thegatewayservice, it cannot reach the database and auth fails for every request. Confirm the gateway'sDB_*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 thewnx_prefix.404 model_not_foundor an empty/v1/modelslist. The model chain is incomplete or inactive. Confirm the Public model, Provider model, Channel, and Channel-provider-model records all exist and areACTIVE, and that the channel has a valid upstream credential./health/readystays503. No channel can serve traffic yet. Finish the provider/channel/model wiring, then check/health/providersfor the per-provider status and theadminandgatewaylogs.- Admin login redirects to HTTPS on a local no-TLS trial. Set
DJANGO_SECURE_SSL_REDIRECT=Falsein.envand recreate theadminservice. Never do this for a real deployment. CHANNEL_ENCRYPTION_KEYSerrors 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.