ai-crew-sync 0.7.1

MCP server that lets a team's AI coding agents (Claude Code, Codex, Cursor or any MCP client) exchange messages, coordinate tasks, share presence and keep shared notes, backed by Postgres
Documentation
# The one compose file. Works with plain compose and with Docker Swarm:
#
#   make up        # = docker compose --project-directory . -f Docker/docker-compose.yml up -d
#   make up-dev    # same, but builds the bus from this checkout (--build)
#   make deploy    # = docker stack deploy -c Docker/docker-compose.yml crew, after a preflight
#
# Every variable has a default so a bare `make up` boots on a laptop; override
# from the environment (or ./.env at the repo root under plain compose — Swarm
# does not read .env, export them or use `env $(cat .env) docker stack deploy`).
# Production safety is NOT enforced here: `make deploy-check` refuses the
# example password, a moving `latest` tag and a missing dashboard secret
# before the cluster is contacted.
#
# The compose project name comes from COMPOSE_PROJECT_NAME in ./.env (a
# top-level `name:` here would break `docker stack deploy`, which rejects
# the property; Swarm names the stack from the CLI argument instead).
services:
  db:
    image: ${POSTGRES_IMAGE:-postgres:18.4-alpine3.24}
    environment:
      POSTGRES_USER: bus
      # Change it for anything reachable from outside your machine.
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me}
      POSTGRES_DB: bus
    volumes:
      # postgres:18+ expects the mount at /var/lib/postgresql (data lives in a
      # versioned subdir), which keeps future pg_upgrade --link possible.
      - bus-data:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U bus -d bus"]
      interval: 5s
      timeout: 3s
      retries: 10
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
        delay: 3s
      resources:
        limits:
          memory: ${POSTGRES_MEMORY_LIMIT:-2g}

  # The broker, for teams routed to JetStream (ADR 0001, phases 4 to 7).
  #
  # **Zero replicas by default**, so a default deployment runs no broker at
  # all and the bus never contacts one. An operator who wants the JetStream
  # path sets NATS_REPLICAS=1 and BUS_NATS_URL, provisions the team's streams
  # with `ai-crew-sync team stream`, and only then routes a team to it. None
  # of those steps implies the next.
  nats:
    image: ${NATS_IMAGE:-nats:2.12-alpine}
    # The configuration is written by the container rather than mounted from
    # the repository. A relative path in `configs.file` resolves against the
    # project directory, and who deploys decides what that is: the Makefile
    # pins it to the repo root, Portainer uses the compose file's own folder.
    # The same file then works for one and fails for the other, which is a
    # deployment failing over a detail that has nothing to do with it.
    #
    # max_payload is why there is a file at all: nats-server takes it only
    # from configuration (there is no flag), and its 1 MiB default refuses a
    # 1 MiB body once envelope headers are added, by about two hundred bytes.
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        cat > /tmp/nats.conf <<'CONF'
        max_payload: 2MB
        http_port: 8222
        jetstream {
            store_dir: /data
            max_memory_store: 256MB
            max_file_store: 8GB
        }
        CONF
        exec nats-server -c /tmp/nats.conf
    volumes:
      - nats-data:/data
    deploy:
      replicas: ${NATS_REPLICAS:-0}
      restart_policy:
        condition: on-failure
        delay: 3s
      resources:
        limits:
          memory: ${NATS_MEMORY_LIMIT:-1g}

  bus:
    # Published multi-arch image by default. `make up-dev` sets BUS_IMAGE and
    # BUS_VERSION to a local tag and passes --build, so a checkout build never
    # shadows the published tag. Production pins an immutable BUS_VERSION.
    image: ${BUS_IMAGE:-ghcr.io/joaquinbejar/ai-crew-sync}:${BUS_VERSION:-latest}
    build:
      # Relative to the project directory (the repo root, which the Makefile
      # pins with --project-directory), NOT to this file's location. Swarm
      # ignores this block (it only deploys images).
      context: .
      dockerfile: Docker/Dockerfile
    environment:
      DATABASE_URL: postgres://bus:${POSTGRES_PASSWORD:-change-me}@db:5432/bus
      BUS_BIND: 0.0.0.0:8787
      # Anti DNS-rebinding. "*" is fine behind a proxy that validates Host;
      # set your real hostname when the bus is exposed directly.
      BUS_ALLOWED_HOSTS: ${BUS_ALLOWED_HOSTS:-*}
      # Empty means "generate at startup": sessions end at restart and are not
      # shared between replicas. Set the same value everywhere in production.
      BUS_DASHBOARD_SECRET: ${BUS_DASHBOARD_SECRET:-}
      # Empty by default: no broker, no JetStream, nothing to configure. Set
      # it to nats://nats:4222 together with NATS_REPLICAS=1 to make the
      # optional path available. It routes nobody on its own.
      BUS_NATS_URL: ${BUS_NATS_URL:-}
      BUS_NATS_CREDENTIALS: ${BUS_NATS_CREDENTIALS:-}
      # Drain the publication outbox here. Turn it off on the replicas that
      # only serve requests when you run dedicated drainers.
      BUS_PUBLICATION_WORKER: ${BUS_PUBLICATION_WORKER:-true}
      # Per replica, on purpose: with N replicas the effective ceiling is N
      # times this. The hard global limit belongs in the proxy.
      BUS_RATE_LIMIT_PER_MINUTE: ${BUS_RATE_LIMIT_PER_MINUTE:-600}
      # Each replica pings itself through Postgres this often to prove its
      # event listener still hears; the ping is also the traffic that keeps
      # Swarm's IPVS from forgetting the idle LISTEN connection (15 min).
      BUS_EVENT_PING_SECS: ${BUS_EVENT_PING_SECS:-30}
      RUST_LOG: ${RUST_LOG:-ai_crew_sync=info}
    ports:
      - "${BUS_PORT:-8787}:8787"
    # Short-form only: Swarm rejects the condition syntax. If the bus starts
    # before the database is ready it exits, and restart (compose) or
    # restart_policy (Swarm) retries until the stack converges.
    depends_on:
      - db
    restart: on-failure
    networks:
      - default
      # The reverse proxy's network. Only used when a Traefik (or any other
      # proxy) lives there; harmless otherwise. It must exist before `up`
      # (the Makefile creates it locally; in Swarm the proxy's stack owns it).
      - edge
    deploy:
      # Stateless (no sessions, identity per request): scale replicas freely
      # behind the routing mesh. Concurrent startup migrations are safe —
      # sqlx takes a Postgres advisory lock.
      replicas: ${BUS_REPLICAS:-1}
      restart_policy:
        condition: on-failure
        delay: 3s
      update_config:
        order: start-first
      resources:
        limits:
          memory: ${BUS_MEMORY_LIMIT:-512m}
      # Traefik v3 (`--providers.swarm`) discovers the bus by these labels when
      # TRAEFIK_ENABLE=true. The proxy owns TLS and the certificate; the bus
      # keeps its plain published port for clients on the private network and
      # is reached at https://${BUS_PUBLIC_HOST}/mcp from the internet.
      labels:
        traefik.enable: "${TRAEFIK_ENABLE:-false}"
        traefik.http.routers.crew-bus.rule: "Host(`${BUS_PUBLIC_HOST:-localhost}`)"
        traefik.http.routers.crew-bus.entrypoints: "${TRAEFIK_ENTRYPOINT:-websecure}"
        traefik.http.routers.crew-bus.tls.certresolver: "${TRAEFIK_CERTRESOLVER:-le}"
        traefik.http.routers.crew-bus.service: "crew-bus"
        traefik.http.services.crew-bus.loadbalancer.server.port: "8787"
        traefik.http.services.crew-bus.loadbalancer.passhostheader: "true"
        # MCP sessions are long-lived streams: do not let the proxy cut them
        traefik.http.services.crew-bus.loadbalancer.responseforwarding.flushinterval: "-1"

networks:
  default:
  edge:
    external: true
    name: ${TRAEFIK_NETWORK:-edge}

volumes:
  bus-data:
  nats-data: