Serve an agent with auth, from init to Docker
In shortScaffold the production template, run it with JWT auth, call it with curl and TenxGraphClient, then generate Docker files with 10xgraph build.
- 4 min read
- 8 sections
- Updated
- v1.0.0rc4
- Markdown
You write the agent. 10xgraph-api turns it into an HTTP service with JWT auth, per-user thread ownership, rate limiting and a hardened container. This page goes from an empty folder to a token-protected server and a Docker build, with the commands and files exactly as the CLI generates them.
Needs: Python 3.12+, pip install "10xgraph[openai]" "10xgraph-api[jwt,redis]" (the template’s own pyproject.toml lists these), OPENAI_API_KEY for the model, and a Redis if you keep the Redis rate limiter. Use --provider google or --provider anthropic to switch the provider extra and the key name.
1. Scaffold the production template
10xgraph init --template production --auth jwt --provider openai --yesThe production template turns on a Redis cache store (read from REDIS_URL), rate limiting and token revocation. The counters are shared between workers. For a first run without Redis, pass --cache-store memory; the counters then live in each process.
The command writes the project into the current folder. The files you will touch:
| File | What it holds |
|---|---|
10xgraph.json |
"agent": "graph.agent:app", "auth": "jwt", "authorization": "ownership", the cache_store block, id_generator, websocket and ag_ui both off |
graph/agent.py |
A ReactAgent compiled with a checkpointer |
.env.example |
MODE="production", ORIGINS, ALLOWED_HOST, JWT_SECRET_KEY, JWT_ALGORITHM, REDIS_URL, OPENAI_API_KEY, empty DOCS_PATH |
tests/, evals/ |
A starter test and a starter eval file |
The ID generator and cache store (rate limit and token revocation) sections of 10xgraph.json as generated:
"id_generator": "uuid",
"cache_store": {
"type": "redis",
"rate_limit": {
"enabled": true,
"requests": 100,
"window": 60,
"by": "user",
"trusted_proxy_headers": false,
"fail_open": false,
"exclude_paths": ["/ping", "/docs", "/redocs", "/openapi.json"]
},
"token_revocation": true
}2. Configure the environment
cp .env.example .envEdit .env:
JWT_SECRET_KEY="<paste the output of: python -c 'import secrets; print(secrets.token_urlsafe(48))'>"
OPENAI_API_KEY="sk-..."
ORIGINS="http://localhost:3000"
ALLOWED_HOST="127.0.0.1,localhost"
REDIS_URL="redis://localhost:6379/0"In production mode the app refuses to start on a wildcard ORIGINS or ALLOWED_HOST, or with API docs enabled (.env.example says so, and you can downgrade these checks with STRICT_PRODUCTION_CHECKS="false"; do not do that outside local tests). Set the two values to your real frontend origin and hostname when you deploy.
3. Check the checkpointer before you deploy
The generated graph/agent.py compiles with InMemoryCheckpointer, which is fine for a first run. The Docker image you build in step 6 starts two Gunicorn workers, and each worker has its own memory, so for anything beyond a demo pass PgCheckpointer to compile():
from tenxgraph.storage.checkpointer import PgCheckpointer
checkpointer = PgCheckpointer(
postgres_dsn="postgresql://user:password@db/agent",
redis_url="redis://redis:6379/0",
)The server uses the checkpointer carried by the compiled graph. The checkpointer key in 10xgraph.json is not read. See Set up checkpointing.
4. Start the server and mint tokens
10xgraph dev --no-open--no-open starts the server without opening the playground. It listens on 127.0.0.1:8000. In a second terminal, mint a token. JwtAuth requires user_id (a non-empty string or a whole number) and exp:
import os
import time
import jwt # PyJWT, installed by 10xgraph-api[jwt]
token = jwt.encode(
{"user_id": "alice", "exp": int(time.time()) + 3600},
os.environ["JWT_SECRET_KEY"],
algorithm=os.environ.get("JWT_ALGORITHM", "HS256"),
)
print(token)In production your login service issues this token. The user_id becomes the owner of every thread the user creates, so it must never come from the request body.
5. Call it
Without a token the server answers 401 and the request never reaches the graph.
export TOKEN="$(JWT_SECRET_KEY=... python make_token.py)"
curl -s http://127.0.0.1:8000/v1/graph/invoke \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":[{"type":"text","text":"Where is order 1042?"}]}]}'The response carries data.meta.thread_id. The server makes thread ids; to continue the conversation send "config": {"thread_id": "<that id>"} in the next request. Another user’s token on that id gets 403.
The same call with the TypeScript client (npm install 10xgraph-client):
import { TenxGraphClient, Message } from '10xgraph-client';
const client = new TenxGraphClient({
baseUrl: 'http://127.0.0.1:8000',
authToken: process.env.TOKEN,
});
const first = await client.invoke([Message.text_message('Where is order 1042?')]);
const threadId = first.meta.thread_id;
const next = await client.invoke([Message.text_message('Can I return it?')], {
config: { thread_id: threadId },
});
console.log(next.messages.at(-1)?.content);For where a browser app should keep the token, see Secure your frontend.
6. Build the container files
10xgraph build --docker-composeThis writes Dockerfile, .dockerignore and docker-compose.yml (add --k8s for a Kubernetes manifest). What the generated files contain:
- A two-stage
Dockerfile: dependencies install in a builder stage, the CLI libraries and pip are removed, and the runtime image runs as the numeric non-root user10001:10001. WEB_CONCURRENCY=2Gunicorn workers withuvicorn_worker.UvicornWorker, andMODE=production.- A
HEALTHCHECKagainst/ping. - In
docker-compose.yml: the port bound to127.0.0.1:8000(put a TLS proxy in front),read_only: truewith/tmpas tmpfs,cap_drop: ALL,no-new-privileges, and a 660 sstop_grace_period. - Secrets come from
.envat run time..dockerignorekeeps.envout of the image.
docker compose up --buildWhat this shows
- Auth is on from the first command: the template writes
"auth": "jwt"and"authorization": "ownership", and a request without a valid token never reaches your graph. See Security in five minutes. - Production mode refuses unsafe settings at start (wildcard origins and hosts, enabled docs). See Production readiness and Security model.
- The container is hardened by
10xgraph build, not by your agent code. See Production checklist and Architecture. - What you must still do: set auth (a server with
auth: nullin production starts open), replace the in-memory checkpointer, and put TLS in front. See What 10xGraph does not protect.
Frequently asked questions
- Does the production template run with no auth?
- No. With --auth jwt the generated 10xgraph.json sets "auth": "jwt" and the server needs JWT_SECRET_KEY. If you set "auth": null and MODE=production, the server starts open and nothing refuses it, so configure auth yourself.
- Why replace InMemoryCheckpointer before deploying?
- The generated image starts Gunicorn with WEB_CONCURRENCY=2, and an in-memory checkpointer lives inside one process. Use PgCheckpointer so threads, ownership and the tool ledger survive a restart and are shared between workers.