# Warmbly — truly local, no-cloud self-host stack.
#
#   docker compose up --build
#
# Brings up the whole platform with NO cloud account of any kind: no AWS, no
# GCP, no Stripe, no Kafka, no Astra. One command, everything local:
#
#   Postgres  — relational store
#   Redis     — cache + realtime pub/sub bridge
#   NATS      — event bus (JetStream); a single ~15MB binary, not Kafka+ZK+SR
#   backend   — API (:8080), applies migrations on boot
#   consumer  — event consumer
#   worker    — send/sync executor (add more for scale; see docs)
#   tracking  — open/click pixels (:3000)
#   realtime  — websocket fanout (:4000)
#   web       — dashboard (:5173)      admin — control panel (:5174)
#   mailpit   — local SMTP sink + web UI (:18025) for testing outbound mail
#
# Providers (all local): EVENTBUS_PROVIDER=nats, KMS_PROVIDER=local,
# BLOB_PROVIDER=filesystem, TASKS_PROVIDER=local, CODEC_PROVIDER=json,
# BILLING_PROVIDER=none, CAPTCHA_PROVIDER=none, PUBSUB_ENABLED=false.
#
# Secrets have dev-friendly defaults so `docker compose up` just works. For a
# real deployment, put your own values in a .env file next to this file:
#   AUTH_SECRET, KMS_LOCAL_MASTER_KEY (base64 32 bytes; `make gen-key`),
#   CREDENTIALS_ENCRYPTION_KEY (64 hex), INTERNAL_API_TOKEN, and — to connect
#   Gmail mailboxes — BOX_GOOGLE_CLIENT_ID / BOX_GOOGLE_CLIENT_SECRET.
#
# LOSING KMS_LOCAL_MASTER_KEY OR CREDENTIALS_ENCRYPTION_KEY IS UNRECOVERABLE:
# every stored mailbox credential is sealed with them. Back them up.
#
# To run on Kafka + S3 instead, set the provider vars above and rebuild the Go
# images with --build-arg GO_TAGS=kafka (tracking: --build-arg CARGO_FEATURES=kafka).

x-selfhost-env: &selfhost-env
  APP_ENV: ${APP_ENV:-dev}
  AWS_CONFIG_ENABLED: "false"
  EVENTBUS_PROVIDER: nats
  NATS_URL: nats://nats:4222
  CODEC_PROVIDER: json
  KMS_PROVIDER: local
  KMS_LOCAL_MASTER_KEY: ${KMS_LOCAL_MASTER_KEY:-Xr0JA7gqF2POy29a7MRByyqddivTNt8WOyKsOXklazk=}
  CREDENTIALS_ENCRYPTION_KEY: ${CREDENTIALS_ENCRYPTION_KEY:-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef}
  BLOB_PROVIDER: filesystem
  BLOB_FS_ROOT: /data/blobs
  BLOB_PUBLIC_BASE_URL: ${API_PUBLIC_URL:-http://${PUBLIC_HOST:-localhost}:8080}/public
  TASKS_PROVIDER: local
  BILLING_PROVIDER: none
  CAPTCHA_PROVIDER: none
  PUBSUB_ENABLED: "false"
  PRIMARY_DB: postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable
  REDIS: redis://redis:6379
  AUTH_SECRET: ${AUTH_SECRET:-local-dev-auth-secret-minimum-32-characters-long}
  INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:-local-dev-internal-token}

services:

  # ─── infrastructure ───────────────────────────────────────────────────

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: warmbly
      POSTGRES_PASSWORD: warmbly
      POSTGRES_DB: warmbly_dev
    ports: ["15432:5432"]
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U warmbly"]
      interval: 5s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7-alpine
    ports: ["16379:6379"]
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 10

  nats:
    image: nats:2.10-alpine
    # -js: JetStream (durable streams). -m 8222: HTTP monitoring for healthcheck.
    command: ["-js", "-sd", "/data", "-m", "8222"]
    ports: ["4222:4222", "8222:8222"]
    volumes:
      - nats_data:/data
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:8222/healthz"]
      interval: 5s
      timeout: 3s
      retries: 10

  # Local SMTP sink + web UI so outbound mail is visible without a real relay.
  # Point mailbox SMTP at mailpit:1025 (or use it as the notification relay).
  mailpit:
    image: axllent/mailpit:latest
    ports:
      - "18025:8025"
      - "11025:1025"
    environment:
      MP_SMTP_AUTH_ACCEPT_ANY: 1
      MP_SMTP_AUTH_ALLOW_INSECURE: 1

  # Local IMAP host for the sandbox demo (`make sandbox`): the mailbox that
  # every seeded sender syncs from, so replies and warmup mail actually land in
  # the unified inbox. Any username logs in with the static password "sandbox";
  # the self-signed cert is fine because sandbox clients set MAIL_TLS_INSECURE.
  # In the `sandbox` profile so the lean `make up` self-host never pulls it —
  # `make sandbox` / `make infra` start it by naming it explicitly.
  dovecot:
    image: dovecot/dovecot:latest
    profiles: ["sandbox"]
    environment:
      USER_PASSWORD: "{PLAIN}sandbox"
    ports:
      - "10143:31143"  # IMAP (cleartext; debugging only)
      - "10993:31993"  # IMAPS (self-signed cert; the sandbox worker + simulator dial this)
    healthcheck:
      test: ["CMD", "doveadm", "service", "status", "imap-login"]
      interval: 5s
      timeout: 3s
      retries: 10

  # ─── application ──────────────────────────────────────────────────────

  backend:
    build:
      context: .
      dockerfile: deploy/docker/backend.Dockerfile
    ports: ["8080:8080"]
    environment:
      <<: *selfhost-env
      API_HOST: "0.0.0.0:8080"
      GIN_MODE: release
      # To reach Warmbly from another machine, set PUBLIC_HOST=<ip-or-domain> in
      # .env — every URL below derives from it. Or set each URL explicitly.
      APP_URL: ${APP_URL:-http://${PUBLIC_HOST:-localhost}:5173}
      CORS_ALLOW_ORIGINS: ${CORS_ALLOW_ORIGINS:-http://${PUBLIC_HOST:-localhost}:5173,http://${PUBLIC_HOST:-localhost}:5174}
      WEBSOCKET_URL: ${WEBSOCKET_URL:-ws://${PUBLIC_HOST:-localhost}:4000/socket/websocket}
      ENCRYPTED_KEYS_PROVIDER: postgres
      EMAIL_NAME: ${EMAIL_NAME:-Warmbly}
      EMAIL_ADDRESS: ${EMAIL_ADDRESS:-noreply@warmbly.local}
      SMTP_HOST: ${SMTP_HOST:-mailpit}
      SMTP_PORT: ${SMTP_PORT:-1025}
      TRACKING_DOMAIN: ${TRACKING_DOMAIN:-${PUBLIC_HOST:-localhost}:3000}
      GEODB_PATH: /app/data/GeoLite2-City.mmdb
      # Gmail mailbox OAuth (leave unset to connect only SMTP/IMAP + Outlook
      # mailboxes). See the self-hosting docs for the Google Cloud setup.
      BOX_GOOGLE_CLIENT_ID: ${BOX_GOOGLE_CLIENT_ID:-}
      BOX_GOOGLE_CLIENT_SECRET: ${BOX_GOOGLE_CLIENT_SECRET:-}
      BOX_OUTLOOK_CLIENT_ID: ${BOX_OUTLOOK_CLIENT_ID:-}
      BOX_OUTLOOK_CLIENT_SECRET: ${BOX_OUTLOOK_CLIENT_SECRET:-}
    volumes:
      - blobs:/data/blobs
    depends_on:
      postgres: { condition: service_healthy }
      redis: { condition: service_healthy }
      nats: { condition: service_healthy }
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
      interval: 10s
      timeout: 3s
      retries: 10
      start_period: 20s

  consumer:
    build:
      context: .
      dockerfile: deploy/docker/consumer.Dockerfile
    environment:
      <<: *selfhost-env
      ENCRYPTED_KEYS_PROVIDER: postgres
    volumes:
      - blobs:/data/blobs
    depends_on:
      backend: { condition: service_healthy }
      nats: { condition: service_healthy }

  # One worker by default. Scale out with `docker compose up --scale worker=3`
  # (or run more on other machines via the SSH enrollment flow). Outbound IPs
  # belong to the mail provider, so more workers = more parallelism, not more IPs.
  worker:
    build:
      context: .
      dockerfile: deploy/docker/worker.Dockerfile
    environment:
      <<: *selfhost-env
      MAIL_TLS_INSECURE: "true"
      ENCRYPTED_KEYS_PROVIDER: http
      ENCRYPTED_KEYS_BACKEND_URL: http://backend:8080
      ENCRYPTED_KEYS_WORKER_TOKEN: ${INTERNAL_API_TOKEN:-local-dev-internal-token}
      BOX_GOOGLE_CLIENT_ID: ${BOX_GOOGLE_CLIENT_ID:-}
      BOX_GOOGLE_CLIENT_SECRET: ${BOX_GOOGLE_CLIENT_SECRET:-}
      BOX_OUTLOOK_CLIENT_ID: ${BOX_OUTLOOK_CLIENT_ID:-}
      BOX_OUTLOOK_CLIENT_SECRET: ${BOX_OUTLOOK_CLIENT_SECRET:-}
    volumes:
      - blobs:/data/blobs
    depends_on:
      backend: { condition: service_healthy }
      nats: { condition: service_healthy }

  tracking:
    build:
      context: ./tracking
      dockerfile: Dockerfile
    ports: ["3000:3000"]
    environment:
      APP_ENV: ${APP_ENV:-dev}
      AWS_CONFIG_ENABLED: "false"
      TRACKING_HOST: "0.0.0.0"
      TRACKING_PORT: "3000"
      EVENTBUS_PROVIDER: nats
      NATS_URL: nats://nats:4222
      KAFKA_TRACKING_TOPIC: tracking-events
      # Overridable so `make dev` (native backend) can point at
      # host.docker.internal:8080 while `docker compose up` uses the container.
      BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL:-http://backend:8080}
      INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:-local-dev-internal-token}
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      nats: { condition: service_healthy }

  realtime:
    build:
      context: .
      dockerfile: deploy/docker/realtime.Dockerfile
    ports: ["4000:4000"]
    environment:
      PHX_HOST: ${PHX_HOST:-${PUBLIC_HOST:-localhost}}
      PORT: 4000
      DATABASE_URL: postgres://warmbly:warmbly@postgres:5432/warmbly_dev?sslmode=disable
      DATABASE_SSL: "false"
      DATABASE_POOL_SIZE: "10"
      REDIS_URL: redis://redis:6379
      # Must equal the backend AUTH_SECRET so JWTs validate across services.
      JWT_SECRET: ${AUTH_SECRET:-local-dev-auth-secret-minimum-32-characters-long}
      SECRET_KEY_BASE: ${SECRET_KEY_BASE:-local-development-secret-key-base-minimum-64-characters-for-phoenix}
      PUBSUB_ENABLED: "false"
      CHECK_ORIGIN: "false"
    depends_on:
      postgres: { condition: service_healthy }
      redis: { condition: service_healthy }

  # Production build of the dashboard (static SPA served by nginx). URLs come
  # from the WARMBLY_* env at container start via /config.js, so the same image
  # the release publishes to GHCR is what runs here. For hot-reload development
  # use `make app` / docker-compose.dev.yml instead.
  web:
    build:
      context: ./web
      dockerfile: Dockerfile
    ports: ["5173:80"]
    environment:
      WARMBLY_API_URL: ${API_PUBLIC_URL:-http://${PUBLIC_HOST:-localhost}:8080}
      WARMBLY_APP_URL: ${APP_URL:-http://${PUBLIC_HOST:-localhost}:5173}
      WARMBLY_TRACKING_DOMAIN: ${TRACKING_DOMAIN:-${PUBLIC_HOST:-localhost}:3000}
      # Captcha is off server-side (CAPTCHA_PROVIDER=none); this Cloudflare test
      # key keeps the widget satisfied without a real key.
      WARMBLY_TURNSTILE_KEY: "1x00000000000000000000AA"
    depends_on:
      backend: { condition: service_healthy }

  admin:
    build:
      context: ./admin
      dockerfile: Dockerfile
    ports: ["5174:80"]
    environment:
      WARMBLY_API_URL: ${API_PUBLIC_URL:-http://${PUBLIC_HOST:-localhost}:8080}
      WARMBLY_DASHBOARD_URL: ${APP_URL:-http://${PUBLIC_HOST:-localhost}:5173}
      WARMBLY_ENV_LABEL: ${ENV_LABEL:-development}
      WARMBLY_TURNSTILE_KEY: "1x00000000000000000000AA"
    depends_on:
      backend: { condition: service_healthy }

  # ─── one-shots ────────────────────────────────────────────────────────

  # Optional demo seed. Run explicitly: docker compose --profile seed run --rm seed
  seed:
    build:
      context: .
      dockerfile: deploy/docker/backend.Dockerfile
    entrypoint: ["/app/seed"]
    environment:
      <<: *selfhost-env
      SEED_RICH: "true"
    depends_on:
      backend: { condition: service_healthy }
    profiles: ["seed"]

volumes:
  postgres_data:
  redis_data:
  nats_data:
  blobs:
