What production-ready means in 10xGraph, and what you still have to do

What 10xGraph runs around your agent by default, the config that turns each protection on, what 1.0 adds, and the honest list of what stays your job.

“Production ready” is an adjective until you can list what it means. 10xGraph is open-source, self-hosted infrastructure for AI applications, and this post lists what that infrastructure does for one setup: you write a graph, compile it, and serve it with 10xgraph-api. For each protection you get the default, the config that changes it, and what stays on your side. It is written for a platform lead evaluating infrastructure, so the “you still must” parts are as long as the “we do” parts.

The short version: you write the agents, tools and tasks, and the server owns the request surface, per-user isolation, run limits and startup checks. The one thing it does not decide for you is whether to turn authentication on. The production template (10xgraph init --template production) does.

1. Who is calling: authentication

Default: none. With auth unset or null, every route is open. This is the quickstart behaviour, and it also holds with MODE=production.

Turn it on: set "auth": "jwt" in 10xgraph.json and export JWT_SECRET_KEY and JWT_ALGORITHM, or point auth at your own BaseAuth subclass. A token needs exp and a non-empty user_id, else the server answers 401. If you set JWT_ISSUER or JWT_AUDIENCE, iss and aud are required too. An HS-family secret shorter than 32 bytes stops the server at startup in production.

You still must: switch it on, keep the signing secret in a secret store, and make your login service the only place that mints user_id. Security in five minutes walks through it.

2. Who owns what: reach and thread access

Default: with auth on, every storage query is scoped to the caller. The default reach is own: only a thread’s owner can read, run or delete it. A role with reach tenant or all can also read, list, delete and stop other users’ threads. Runs and writes always stay with the caller’s own threads, admins included. A thread outside the caller’s reach answers 404 before the graph starts, so a foreign id looks the same as a missing one. A client cannot invent a thread id: it starts a thread by omitting the id and uses the one the server returns.

Turn it on or tune it: pass a checkpointer to graph.compile(checkpointer=...). The checkpointer key in 10xgraph.json is not read by the server. Set roles with a reach and tenant_claims inside the auth object. runs.thread_ids: "client" opts out of server-made ids.

You still must: use a checkpointer that supports owners (PgCheckpointer, SQLite, in-memory), declare tenant_columns if you serve several organizations, and think about who may share data. The companion post Why a user cannot read another user’s thread shows the mechanism in detail, and Multi-tenant data covers the columns.

3. What a request may do: roles and permissions

Default: routes check resource:action permissions such as graph:invoke. At startup a route guard refuses to boot if any non-public route has no permission guard. Only /ping and /health/ready are public. Graph introspection needs graph:read. With no roles configured, every caller gets the default set (all but admin:*); a token scope or scopes claim can only narrow it.

Config: add roles to the auth object, each with permissions and a reach, or override BaseAuth.authorize. See Auth.

You still must: issue narrow roles, and check permissions inside tools that touch sensitive data. Permissions apply per route, not per tool: the server authorizes the request, not the business logic inside your tool.

4. What a client may send: input shaping

Default: client messages must have the user role. An assistant or system message is refused with 422, so a forged “I approved it” cannot enter the thread. A tool message is accepted only as the answer to a pending remote tool call. context_summary and execution_meta are never client-writable, the server replaces client message ids, and reserved config keys such as user, user_id, tool_timeout and max_parallel_tool_calls are rejected.

Config: runs.config_keys and runs.state_keys are allow-lists for request config and state (422 on other keys).

By design: a client may set custom state fields and current_node. That lets a frontend picker write a selected id straight into state, and lets a dropdown of agents start at the chosen node. If your graph needs an entry guard, enforce it inside the graph or restrict runs.state_keys.

You still must: treat anything the client may put into state as untrusted input in your own nodes.

5. How much a run may cost: limits

Defaults:

  • Request bodies are capped at 10 MB (MAX_REQUEST_SIZE, 413 over it).
  • Each user may have 10 HTTP runs in flight (runs.max_concurrent_per_user, 429 over it).
  • In production a run that exceeds 300 seconds is stopped: invoke returns 504, a stream ends with an error chunk (runs.timeout).
  • A thread runs one request at a time. A second request waits up to 30 seconds, then gets 409 “thread is busy”. on_busy: "cancel" stops the running one instead.
  • A model turn runs at most 10 tool calls at once (runs.max_parallel_tool_calls).

Config: cache_store.rate_limit (by ip, user or global) is off until you add it to 10xgraph.json; the production template adds it. Its counters live in the API’s cache store, in memory or in Redis (cache_store.type). websocket.max_connections and websocket.max_connections_per_user cap sockets.

You still must: add rate limiting, and set trusted_proxies to the real number of proxy hops. Streaming runs have no per-node or per-tool deadlines, only the run timeout.

6. What the server reveals: production mode

Default with MODE=production: an unknown MODE value refuses to start. A wildcard ORIGINS or ALLOWED_HOST refuses to start (STRICT_PRODUCTION_CHECKS is on by default in production). API docs are off, graph introspection is scoped, debug output is off, and 5xx bodies and error chunks carry generic text. A node exception reaches the client as 500 NODE_001 “An error occurred in a graph node.”, with no stack trace. A tool that raises reaches the model only as “failed with an internal error (reference …)”; the raw text stays in the server log under that reference.

Config: MODE, ORIGINS, ALLOWED_HOST, STRICT_PRODUCTION_CHECKS.

You still must: set the real origins and hosts, and read your logs, because the detail moved there.

7. Outbound requests: SSRF and files

Default: external media URLs in client input are checked against the graph’s MediaFetchPolicy before the run, and a blocked URL is a 400. Downloads and the prebuilt fetch_url tool resolve the host once, require public addresses, pin the connection, re-check each redirect and cap the body. The prebuilt file tools are confined to a root you set in code.

Config: media_policy on compile(), create_file_tools(root=...).

You still must: use those prebuilt tools, or apply the same discipline in your own. A hand-written tool that fetches arbitrary URLs is outside the policy.

8. Durable and replay-safe state

Default with PgCheckpointer: state is in PostgreSQL with Redis as a hot cache. Writes are versioned per thread, so a stale run cannot move the cache backwards. A tool call that finished before a crash is recorded in a ledger and not run again on resume (invoke path). A crash before the tool returns can still repeat it. Background: hot and cold agent memory and your agent charged the card twice.

Config: PgCheckpointer passed to compile(), with DATABASE_URL and REDIS_URL.

You still must: back up PostgreSQL and decide retention. The ledger gives at-least-once safety, not exactly-once, so give money-moving tools an idempotency key. Streaming runs do not consult the ledger.

9. Deploy and scale

Default: 10xgraph build writes a Dockerfile that runs Gunicorn as a non-root user, and with flags a docker-compose.yml and k8s.yaml. GET /ping is liveness and GET /health/ready is readiness. Workers hold no conversation state; it lives in the checkpointer. With Redis configured as the cache_store, run caps, thread locks, rate-limit counters and token revocations are shared across workers.

Config: 10xgraph build --docker-compose --k8s, cache_store with type: "redis" (or type: "custom" for your own BaseCacheStore), cache_store.token_revocation: true.

You still must: terminate TLS at your load balancer or ingress, run two or more instances if you need availability, and size your database.

What 1.0 adds

These ship in 1.0. Each is opt-in through config unless the production template writes it for you.

  • Background jobs. A thin TaskIQ layer: 10xgraph worker runs tasks, 10xgraph scheduler fires scheduled ones, and /v1/tasks routes start, read, cancel and reschedule them. With auth on, a task runs under the caller’s identity and needs the tasks:trigger permission. Delivery is at-least-once: if a worker dies, another runs the task again, so write tasks that are safe to rerun. See Background tasks.
  • Metrics. A metrics block in 10xgraph.json turns on Prometheus scraping or OTLP push for the API and the worker. Dashboards and alert rules are yours. See Prometheus and Grafana.
  • JSON logs and an audit log. LOG_FORMAT=json makes every application log line one JSON object. The audit block adds a metadata-only audit log: one event per permission check, authentication failure or change, with ids and outcomes and never prompts, answers or file contents. 10xGraph produces the stream and does not store it; you ship it to your own sink and own its retention. See Audit log.
  • A control mapping. Built for teams going through SOC 2 and ISO 27001 audits: the control mapping lists which controls the platform gives you evidence for, what is on by default, and what stays with your organization. 10xGraph holds no audit report of its own.

The honest list

Regardless of how many boxes above are ticked:

  • Auth is not forced. auth: null serves everyone, including in production. The production template turns it on; the bare server does not.
  • Prompt injection is not solved. Validators and approval steps bound what a model can be talked into. They do not remove it.
  • Your tools are your code. The server cannot know what a tool should be allowed to read.
  • Secrets, TLS, backups, retention are infrastructure you run.
  • 10xGraph needs Python 3.12 or newer and has a smaller community than LangGraph or CrewAI. Pin versions and read the changelog.
  • Exactly-once is not on offer. Replayed tools and jobs are at-least-once. Send idempotency keys to anything that moves money.

Where to go next

The platform overview has the full table with links to each reference page. The security model lists every threat and the six curl checks to verify it yourself, and What 10xGraph does not protect expands the list above.

Frequently asked questions

Is 10xGraph production ready?
The server enforces reach-scoped thread access, permission checks, input limits, run limits and production-mode checks by default, and keeps state durably with PostgreSQL. You still own auth configuration, secrets, TLS, backups and your tools' own authorization.
Does the server force me to configure authentication?
No. With auth set to null the server starts and is open, also with MODE=production. Nothing refuses it, so set auth to jwt or a custom BaseAuth class before you expose the server.
Does 10xGraph stop prompt injection?
No. Validators, approval steps and tool allow-lists bound what a model can be talked into, but they do not remove prompt injection. The limits page lists what the server does not protect.

Written by

Shudipto Trafder

Maintainer, 10xGraph

Keep reading

Subscribe via RSS

5 min read

A prompt-injected model cannot skip your approval step

How 10xGraph keeps handoffs, tool visibility, tool results and interrupt resumes under the graph's control, so a steered model cannot walk around a human approval.

  • security
  • interrupts
  • human-in-the-loop

Blog home Archive