# Every knob ai-crew-sync reads, with its default. Copy to `.env` and change
# what you need — a bare `make up` works with this file absent, because the
# compose file carries the same defaults.
#
# THESE ARE LOCAL DEFAULTS. The same compose file serves production; what
# keeps a production stack safe is `make deploy-check` (run by `make deploy`),
# which refuses until POSTGRES_PASSWORD, BUS_VERSION, BUS_ALLOWED_HOSTS and
# BUS_DASHBOARD_SECRET are set, and rejects the example password and a moving
# `latest` tag.
#
# Docker Swarm does NOT read .env files: export the variables, or use
# `env $(cat .env | xargs) docker stack deploy -c Docker/docker-compose.yml crew`.
# ---------------------------------------------------------------- compose --
# Keeps container and volume names stable regardless of directory layout.
COMPOSE_PROJECT_NAME=ai-crew-sync
# Database password. CHANGE IT for anything reachable from outside this
# machine — it is the only credential in front of your team's traffic.
POSTGRES_PASSWORD=change-me
# Image tags the stack runs. `latest` is fine locally; production must pin an
# immutable release (the preflight rejects `latest`), so a rolling restart
# cannot silently change the running version. BUS_IMAGE is the repository
# without tag; `make up-dev` sets both to a local tag (ai-crew-sync:dev) so a
# checkout build never shadows the published tag.
BUS_VERSION=latest
BUS_IMAGE=ghcr.io/joaquinbejar/ai-crew-sync
POSTGRES_IMAGE=postgres:18.4-alpine3.24
# Host port the bus is published on. The container always listens on 8787.
BUS_PORT=8787
# Bus replicas. The server is stateless, so this scales horizontally behind
# the Swarm routing mesh; plain compose ignores values above 1.
BUS_REPLICAS=1
# Memory ceilings (deploy.resources.limits; Swarm and recent compose honour them).
BUS_MEMORY_LIMIT=512m
POSTGRES_MEMORY_LIMIT=2g
# Multi-node swarm: pin the stateful services to the node that holds their
# node-local volume, or a reboot can reschedule them onto an empty one.
# Unset (the default) means no pin, which is right for a single host.
# POSTGRES_PLACEMENT=node.hostname == db-node
# NATS_PLACEMENT=node.hostname == broker-node
# Reverse proxy (Traefik v3 with `--providers.swarm`). The bus always joins
# the proxy's network (TRAEFIK_NETWORK, created locally by `make up` if
# missing; in Swarm the proxy's stack owns it as an attachable overlay). The
# router labels only take effect with TRAEFIK_ENABLE=true, which then requires
# BUS_PUBLIC_HOST (the preflight enforces it).
TRAEFIK_ENABLE=false
TRAEFIK_NETWORK=edge
TRAEFIK_ENTRYPOINT=websecure
TRAEFIK_CERTRESOLVER=le
# ----------------------------------------------------------------- server --
# Read by `ai-crew-sync serve`, and DATABASE_URL by the admin subcommands.
# Where the server finds Postgres. Under compose this is derived from
# POSTGRES_PASSWORD and needs no setting; export it when running the binary
# directly or using the admin CLI from your own shell.
# DATABASE_URL=postgres://bus:change-me@localhost:5432/bus
# Address to bind inside the container/process.
BUS_BIND=0.0.0.0:8787
# Comma-separated hostnames accepted in the Host header (anti DNS-rebinding).
# "*" accepts any host — fine behind a proxy that already validates it; set
# your real hostname when the bus is exposed directly.
BUS_ALLOWED_HOSTS=*
# Public hostname the reverse proxy routes to the bus, e.g. crew.example.com.
# Only the Traefik labels in the compose file read it (with TRAEFIK_ENABLE=true).
BUS_PUBLIC_HOST=
# Comma-separated browser origins accepted by CORS. Empty disables the check
# (the dashboard is same-origin, so it does not need this).
BUS_ALLOWED_ORIGINS=
# Run pending migrations on startup. Turn it off to run them as a separate
# release step (`ai-crew-sync migrate`) instead.
BUS_AUTO_MIGRATE=true
# Largest MCP request body accepted, in bytes. Rejected with 413 before the
# JSON is parsed. The default clears the biggest legitimate request — a 1 MiB
# body plus eight 256 KiB attachments, which base64 inflates to ~2.7 MiB.
BUS_MAX_REQUEST_BYTES=8388608
# Requests per minute per token, enforced in-process (0 disables it). With
# several replicas the ceiling is per replica: use the reverse proxy when you
# need a hard global limit.
BUS_RATE_LIMIT_PER_MINUTE=600
# Seconds between the pings each replica sends itself through Postgres to
# prove its event listener still hears; three unanswered and it reattaches,
# and /health reports the state under `events`. The ping is also the traffic
# that keeps Swarm's IPVS from forgetting an idle LISTEN connection.
BUS_EVENT_PING_SECS=30
# Signs the dashboard's read-only session cookies. Set the same value on every
# replica so a session works across all of them; when unset, a random key is
# generated at startup and sessions end at restart.
# BUS_DASHBOARD_SECRET=
# Log filter (tracing-subscriber syntax).
RUST_LOG=ai_crew_sync=info
# ------------------------------------------------- console client + plugin --
# Read by `ai-crew-sync client ...` and by the Claude Code plugin hooks.
# MCP endpoint and the personal token of one agent (from `agent add`).
# The plugin's hooks stay silent when these are unset.
# BUS_URL=http://localhost:8787/mcp
# BUS_TOKEN=acs_...
# Local profile to connect with, and the directory whose .acs.toml project
# defaults apply (the current directory by default). See
# `ai-crew-sync context show` for the precedence between these and the
# explicit BUS_URL/BUS_TOKEN above.
# BUS_PROFILE=acme
# BUS_PROJECT_DIR=
# Lifecycle hooks: BUS_DIGEST_HOURS also bounds what `context hook
# --event session_start` summarises (1-336).
# Conversation id for `ai-crew-sync mcp proxy`, for hosts that can set a
# per-window variable. The same id always maps to the same bus session, so a
# resumed conversation reconnects to its own; omit it and the proxy process is
# the conversation.
# BUS_HOST_SESSION=
# Lifetime the proxy asks for its session credential (seconds; the bus bounds
# it to 60..86400, default 24 h) and how long before expiry it renews it
# (default: half the lifetime). Renewal keeps secret and epoch; only the
# expiry moves.
# BUS_SESSION_TTL_SECS=86400
# BUS_SESSION_RENEW_LEAD_SECS=43200
# Which working context this shell is — usually the repository name. One
# token, one person; one session per repository you work in, so your presence,
# task claims and locks are not shared between unrelated pieces of work.
# Sent as the X-Crew-Session header. Unset means the shared session, which is
# how the bus behaved before sessions existed.
# BUS_SESSION=market-data
# ------------------------------------------------------------- JetStream --
# Phase 4 of ADR 0001. Nothing here is read unless a team is explicitly
# routed to the JetStream backend; a default install needs no broker.
# Broker URL. Inside the stack it is the nats service; TLS in production.
# Setting it routes nobody: see `team capability --backend`.
# BUS_NATS_URL=nats://nats:4222
# The stack's own broker. 0 means no broker runs at all, which is what a
# default deployment wants. Set it to 1 together with BUS_NATS_URL.
# NATS_REPLICAS=0
# NATS_IMAGE=nats:2.12-alpine
# NATS_MEMORY_LIMIT=1g
# Runtime credential: publish and fetch only. Provisioning streams uses a
# different credential that the server process must NOT hold.
# BUS_NATS_CREDENTIALS=/etc/ai-crew-sync/runtime.creds
# The broker itself needs `max_payload: 2MB` in its configuration file
# (`nats-server -c nats.conf`; there is no command-line flag): its default of
# 1 MiB refuses a 1 MiB body once envelope headers are added. Raising only the
# stream's max_message_size is not enough. Docker/nats-test.conf is a working
# example, and the compose file's nats service writes the same file.
# Drain the publication outbox in this process. Leave it on unless you run
# dedicated drainer replicas, and turn it off on the replicas that only serve
# requests. With no BUS_NATS_URL there is nothing to drain either way.
# BUS_PUBLICATION_WORKER=true
# ------------------------------------------------------ remote admin CLI --
# Read by `ai-crew-sync admin ...` (everything but `admin bootstrap`).
# Where `admin login` stores the credential and `--save` writes tokens-<team>
# files. Default: $XDG_CONFIG_HOME/ai-crew-sync, else ~/.config/ai-crew-sync.
# BUS_CONFIG_DIR=
# Bypass the stored login, for scripts and CI: the bus base URL (no path) and
# an administrative credential (acsa_..., from `admin bootstrap`/`admin grant`).
# BUS_ADMIN_URL=https://bus.example.com:8443
# BUS_ADMIN_TOKEN=acsa_...
# Plugin only: seconds each hook waits on the bus before giving up, and how
# many hours of team activity the session-start catch-up summarises.
BUS_TIMEOUT=8
BUS_DIGEST_HOURS=8
# Seconds SessionStart waits for a resumed conversation's new proxy to resume
# its session before it reports the credential gone (0-10; other values: 6).
BUS_RESUME_WAIT_SECS=6
# Seconds the background watcher waits on the bus for an idle window before
# giving up quietly (60-86400); keep it under the plugin's hook timeout (4 h).
BUS_WATCH_SECS=14000