Deployment
Tares is a single process writing to a single DuckDB file. There is no external database or broker
to run. DuckDB is single-writer, so a deployment is exactly one taresd and one data volume.
Do not run multiple replicas against the same data.
Local
Install the package and run tares up:
uv tool install tares # or: pipx install tares
tares up # console on http://127.0.0.1:8787, data in ~/.tarestares up accepts --host, --port, --data-dir, --open, and --auth (see
Authentication, off by default).
To expose the MCP endpoint for external agents, also run tares mcp as a second process:
tares mcp --transport streamable-http --port 8788 --taresd http://localhost:8787The MCP endpoint is then at http://localhost:8788/mcp.
Docker
One image serves both processes: it runs the daemon by default, and the MCP server when started
with the tares mcp command. Run them as two containers on a shared network so they can reach
each other:
docker network create tares
docker run -d --name taresd --network tares -p 8787:8787 -v tares-data:/data \
ghcr.io/glassflow/tares:latest
docker run -d --name tares-mcp --network tares -p 8788:8788 \
ghcr.io/glassflow/tares:latest \
tares mcp --transport streamable-http --host 0.0.0.0 --port 8788 --taresd http://taresd:8787The console is at http://localhost:8787, the MCP endpoint at http://localhost:8788/mcp.
(Only want the console? The first two commands are enough.) Running both plus TLS is what the
compose file below wires up for you.
To tear it down:
docker rm -f taresd tares-mcp
docker network rm tares
docker volume rm tares-data # only if you also want to delete the dataServer (compose + TLS)
For a server, run the daemon, the MCP server, and a reverse proxy that terminates TLS and routes one
hostname. Two files in one directory: docker-compose.yml and the Caddyfile it mounts. Both ship
in the repo under deploy/compose/,
or copy them from here. The image serves both processes (same image, different command):
# docker-compose.yml
name: tares
services:
taresd:
image: ghcr.io/glassflow/tares:${TARES_VERSION:-latest}
command: ["tares", "up", "--host", "0.0.0.0", "--data-dir", "/data"]
environment:
TARES_AUTH_TOKEN: "${TARES_AUTH_TOKEN:?set TARES_AUTH_TOKEN}"
volumes: [tares-data:/data]
expose: ["8787"]
restart: unless-stopped
mcp:
image: ghcr.io/glassflow/tares:${TARES_VERSION:-latest}
command: ["tares", "mcp", "--transport", "streamable-http", "--host", "0.0.0.0",
"--port", "8788", "--taresd", "http://taresd:8787"]
environment:
TARES_AUTH_TOKEN: "${TARES_AUTH_TOKEN:?set TARES_AUTH_TOKEN}"
expose: ["8788"]
depends_on: [taresd]
restart: unless-stopped
caddy:
image: caddy:2
ports: ["80:80", "443:443"]
environment:
TARES_DOMAIN: "${TARES_DOMAIN:-:80}"
volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "caddy-data:/data"]
depends_on: [taresd, mcp]
restart: unless-stopped
volumes:
tares-data:
caddy-data:Save this next to it as Caddyfile — the compose file mounts it into the Caddy container
(./Caddyfile:/etc/caddy/Caddyfile), so compose fails to start without it:
# Caddyfile: one hostname; /mcp and /sse go to the MCP server, the rest to the daemon
{$TARES_DOMAIN} {
handle /mcp* { reverse_proxy mcp:8788 }
handle /sse* { reverse_proxy mcp:8788 }
handle { reverse_proxy taresd:8787 }
}Set the domain and token, then start it from that directory:
export TARES_AUTH_TOKEN=$(openssl rand -hex 24)
TARES_DOMAIN=tares.example.com docker compose up -dA real TARES_DOMAIN enables Caddy’s automatic HTTPS (point its DNS A record at the host first);
unset, it serves plain HTTP on :80. The console is at https://<domain>; the MCP endpoint at
https://<domain>/mcp.
Image versions
Images are published to ghcr.io/glassflow/tares: :latest (the default branch) and a tag per
release (:0.1.0, :0.1). Pin TARES_VERSION for reproducible deploys.
Configuration
Both processes are configured by environment variables. The read by column says which process
uses each one: the daemon (taresd), the MCP server (tares-mcp), or both.
| variable | read by | default | description |
|---|---|---|---|
TARES_DB | taresd | tares.duckdb | path to the DuckDB file (tares up uses ~/.tares) |
TARES_HOST | taresd | 127.0.0.1 | bind address (0.0.0.0 to expose) |
TARES_PORT | taresd | 8787 | bind port |
TARES_CATALOG | taresd | catalog.yaml | catalog YAML imported on first boot if the DB is empty |
TARES_CATALOG_SYNC | taresd | unset | if set, re-import the catalog YAML on every boot (file is source of truth) |
TARES_AUTH_TOKEN | both | unset | require this bearer token everywhere (what tares up --auth sets); tares-mcp also sends it to the daemon |
TARES_OTLP_GRPC_PORT | taresd | 4317 | OTLP/gRPC receiver port (off to disable; needs the otlp-grpc extra) |
ANTHROPIC_API_KEY | taresd | unset | model key for the Ask assistant and Tares agents (or set one in the console) |
ANTHROPIC_AUTH_TOKEN | taresd | unset | a bearer token instead of the key, for a gateway that wants Authorization: Bearer; beats the key when both are set |
ANTHROPIC_BASE_URL | taresd | https://api.anthropic.com | an Anthropic-compatible gateway to send model calls through; TARES_ANTHROPIC_BASE is the old name, still read |
TARES_AGENT_MODEL | taresd | claude-sonnet-4-6 | model used by the Ask assistant and Tares agents |
TARESD_URL | tares-mcp | http://127.0.0.1:8787 | the daemon to proxy to (what --taresd sets) |
TARES_MCP_TRANSPORT | tares-mcp | stdio | stdio, streamable-http, or sse |
TARES_MCP_HOST | tares-mcp | 127.0.0.1 | bind address for the HTTP transports |
TARES_MCP_PORT | tares-mcp | 8788 | bind port for the HTTP transports |
Authentication
Auth is one switch, set at launch:
tares up: open. No login; the API, console, and ingest are all reachable without a credential. The local default.tares up --auth: secured. Every route requires a credential: the console and API, and ingest too. A bare--authgenerates a root token, persists it to the data dir, and prints a click-to-login URL each launch (…/?token=<root>).--auth=<token>(or settingTARES_AUTH_TOKEN) uses your own token, the shape for hosted/scripted deploys.
There is no separate ingest token and no read-only mode. On a secured instance you hand machines their own scoped API keys, never the root token.
API keys
The root token is the operator’s login; you mint narrower credentials from it. In the console → Settings → API keys, create a key with a subset of three scopes:
| scope | grants |
|---|---|
read | queries, timelines, catalog, an agent’s own views & subscriptions (the MCP read surface) |
ingest | POST /ingest/*, /v1/*, write memories |
admin | sources / views / triggers, key management (implies the others) |
Keys are shown once at creation, revocable, and never returned again. Example key types:
| holder | scopes |
|---|---|
| an MCP agent | read |
| a producer (Vercel drain, OTLP exporter, webhook) | ingest |
| the Claude Code plugin | read + ingest |
On a secured instance, creating a push source mints an ingest key for it and shows it once.
Hand that to the producer as Authorization: Bearer …. It appears in Settings → API keys like
any other key, where you can mint new ones and deactivate existing ones.
The root token is the console login. For per-user SSO, put a proxy (oauth2-proxy, Tailscale, Caddy basic-auth) in front of the console; machines keep their scoped API keys.
Settings
Settings in the console footer holds the instance’s access mode and credentials, in four tabs
(/settings?tab=… deep-links each):
- Access and API keys: the auth mode this instance was launched with, and the API keys.
- Model access: what Tares agents and Ask run on. First the choice of
where model calls go, Anthropic directly or through a gateway; then the
key, or the gateway’s URL and token. Values stored here win over the environment
(
ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_BASE_URL); with none stored, the environment’s values show as such. Credentials are never returned by the API. - GitHub: a GitHub token stored once, by name. Sources of the
githubconnector reference it ascredential: <name>instead of a token per source, and MCP servers reference it as their auth valuecredential:github/<name>. Test shows the login it belongs to; the row lists which sources and MCP servers use it; Rotate replaces the token in one place and every user of it follows on its next poll or connection. The token is never returned by the API and never included in a catalog export. Use a fine-grained token with Contents (read, or read and write on a repository an agent should update), Pull requests read and write, Metadata read. - Slack: the workspace bot token findings and firings are posted with, and the signing secret behind inbound Slack events.
Using an LLM gateway
Tares agents and Ask talk to api.anthropic.com by default. An organisation that routes all model traffic through a gateway (LiteLLM, Portkey, Helicone, a proxy in front of Bedrock or Vertex) can point Tares at it. Two ways, and both reach every model call, for trigger-woken agents and for Ask alike.
In the console, under Settings → Model access: choose through a gateway, enter the gateway’s URL and the token it issued, save. The change applies from the next model call, no restart. This is the path on a hosted cell, where the environment is not yours to set; the platform key stops being used, exactly as when you store your own key. Stop using the gateway returns to the environment’s gateway if the deployment set one, else to Anthropic.
In the environment, with the same variables Claude Code uses:
ANTHROPIC_BASE_URL=https://llm-gateway.internal # serves the Anthropic Messages format
ANTHROPIC_AUTH_TOKEN=<token the gateway issued> # sent as Authorization: Bearer
tares upValues stored in the console win over the environment. The credential is sent one of two ways: a
gateway token (stored, or ANTHROPIC_AUTH_TOKEN) as a bearer header, or a key (stored, or
ANTHROPIC_API_KEY) as the Anthropic key header, which gateways accept too. A stored gateway token
comes first, then a stored key, then the environment’s token, then its key.
Two things to know when a gateway is in the path:
- The gateway decides what answers. Tares sends a Claude model id and accepts any well-formed reply; a gateway that maps that id to another model is the operator’s choice. The agent prompts and the builder are written and tested against Claude.
- Spend is priced at Claude rates. The console’s spend meter prices runs from Tares’s own price table, by the model name in the reply. Behind a gateway, the gateway’s bill is the truth; a reply naming a model Tares does not price shows as an uncosted run.
The environment variables are read when taresd starts; the console values are read on every
call.
Upgrading from 0.x
1.0 renamed the project from NavFlow to Tares. There is no compatibility layer: the old names are not read at all. If the daemon finds leftovers from a pre-1.0 install, an old environment variable or an old database file, it stops at startup and tells you exactly what to rename.
Environment variables. Every NAVFLOW_* variable is now TARES_*, with the same meaning.
Nothing falls back to the old name. If any NAVFLOW_* variable is still set, tares up and
taresd exit and print the exact renames you need:
tares 1.0 renamed every NAVFLOW_* environment variable to TARES_*, and does NOT read the old names.
Still set in this environment:
NAVFLOW_DB -> TARES_DB
Rename them and start again.Three variables are gone rather than renamed: NAVFLOW_INGEST_TOKEN and NAVFLOW_READONLY were
folded into --auth plus scoped API keys in 0.2.0, and NAVFLOW_ANTHROPIC_KEY became
the standard ANTHROPIC_API_KEY.
The database file. navflow.duckdb is now tares.duckdb. DuckDB creates a missing file
happily, so an upgrade that left the old file in place would come up healthy and completely empty
with your data untouched beside it. Instead the daemon stops:
found a pre-1.0 database at ~/.tares/navflow.duckdb, and none at ~/.tares/tares.duckdb.Migrating is one move, with the daemon stopped:
mv ~/.tares/navflow.duckdb ~/.tares/tares.duckdb # or /data/… in a containerThe data directory itself also moved, from ~/.navflow to ~/.tares. If you never set
--data-dir or TARES_HOME, move the whole directory (mv ~/.navflow ~/.tares) before
renaming the file inside it.
Everything else is a straight rename, with no runtime guard: the package and command
(navflow to tares), the daemon (navflowd to taresd), the MCP proxy (navflow-mcp to
tares-mcp), the --navflowd flag (--taresd), the image (ghcr.io/glassflow/navflow to
ghcr.io/glassflow/tares), and the Claude Code plugin (navflow@navflow to tares@tares).
Backups
The data is the DuckDB file in the volume. Back it up by snapshotting the volume (or the host) or
copying /data/tares.duckdb; restore by putting the file back before start.