Configuración: referencia de variables de entorno

Por: Artiko
buzznostrrelayconfiguracionenvdevops

Configuración: referencia de variables de entorno

En el capítulo 3 levantaste un relay con la configuración que just bootstrap copió desde .env.example. Funcionó porque cada default de ese archivo apunta a los contenedores de docker-compose.yml. Ese es exactamente el problema que resuelve este capítulo: los defaults de desarrollo son cómodos y varios de ellos son inseguros en producción.

Este capítulo es una referencia. Recorre bloque por bloque las dos plantillas del repositorio, contrasta lo que dice la plantilla contra lo que el binario realmente lee en crates/buzz-relay/src/config.rs, y marca qué hay que cambiar antes de exponer el relay.

Las tres capas de configuración

Buzz no tiene archivo de configuración propio. Todo entra por variables de entorno. Lo que cambia es quién las pone en el entorno del proceso.

flowchart TD
    A[".env en la raiz del repo"] -->|"set dotenv-load := true en el Justfile"| P["Entorno del proceso buzz-relay"]
    B["Entorno del shell o del systemd unit"] --> P
    C["deploy/compose/.env via env_file"] --> D["Contenedor relay"]
    E["Bloque environment de compose.yml"] -->|"pisa lo anterior"| D
    D --> P
    P --> F["Config::from_env"]
    F --> G["Arranque o error fatal de configuracion"]

Tres consecuencias prácticas:

  1. En desarrollo, Justfile:3 declara set dotenv-load := true. Todas las recetas de just cargan .env antes de ejecutar nada. No necesitas source .env.
  2. En el bundle de Compose de producción, el servicio relay usa env_file: [.env] y además un bloque environment: explícito. Las claves del bloque environment: ganan sobre las del .env. Ahí es donde deploy/compose/compose.yml fija BUZZ_BIND_ADDR, BUZZ_HEALTH_PORT, BUZZ_METRICS_PORT, DATABASE_URL, REDIS_URL, BUZZ_S3_ENDPOINT, BUZZ_S3_ADDRESSING_STYLE y BUZZ_GIT_REPO_PATH. Ponerlas en tu .env no tiene efecto.
  3. El binario tiene sus propios defaults, que no siempre coinciden con la plantilla. Si una variable no está definida, manda config.rs, no .env.example.

Las dos plantillas

ArchivoLíneasPúblicoFilosofía
.env.example (raíz)252Desarrollo localTodo funciona con docker compose up sin editar nada
deploy/compose/.env.example52VPS de producciónTodos los secretos son literales CHANGE_ME que hay que reemplazar

La plantilla de producción es deliberadamente corta: solo trae lo que hay que decidir. El wrapper deploy/compose/run.sh aborta si queda cualquier línea con CHANGE_ME antes de start, restart, pull, upgrade o config.

Cómo se parsean los valores

No hay un parser único. Conviene conocer las tres familias, porque escribir TRUE en la variable equivocada es un no-op silencioso:

FamiliaFunciónAcepta como verdaderoComportamiento con basura
Booleano estrictoparse_bool en config.rs:394true, 1, onError fatal de configuración
Booleano laxocomparación inlinesolo "true" o "1" exactosSe interpreta como false, sin aviso
Entero positivopositive_u64_from_env en config.rs:301cualquier entero mayor que ceroError fatal

BUZZ_REQUIRE_AUTH_TOKEN, BUZZ_REQUIRE_RELAY_MEMBERSHIP, BUZZ_PUBKEY_ALLOWLIST y BUZZ_ALLOW_NIP_OA_AUTH usan la familia laxa: BUZZ_REQUIRE_AUTH_TOKEN=on no activa nada. BUZZ_AUTO_MIGRATE es más permisiva y acepta true, 1, yes u on (main.rs:29-36). BUZZ_MESH acepta on, true o 1, ignorando mayúsculas solo para on. BUZZ_STORAGE_METRICS es la inversa: cualquier valor distinto de off deja la función encendida.


Bloque 1: base de datos y read replica

VariableDefault del binarioPara qué sirveCuándo tocarla
DATABASE_URLpostgres://buzz:buzz_dev@localhost:5432/buzzURL del Postgres writerSiempre en producción
READ_DATABASE_URLsin definirRéplica de lectura opcional; vacío mantiene todas las lecturas en el writerSolo si tienes réplica
BUZZ_DB_POOL_SIZE50Conexiones máximas del pool writerSegún carga y max_connections de Postgres
BUZZ_DB_READ_POOL_SIZEsin definir, cae al valor del writerPool del readerSi la réplica tiene otro dimensionamiento
BUZZ_REPLICA_READ_MAX_AGE_MS0, es decir apagadoFrescura máxima tolerada al enrutar lecturas a la réplica, en milisegundosAl habilitar lecturas en réplica
BUZZ_AUTO_MIGRATEdesactivado si no se defineEjecuta las migraciones SQLx embebidas al arrancarDecisión consciente, ver abajo
PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASElocalhost, 5432, buzz, buzz_dev, buzzVariables de psql para los scripts de desarrolloSolo desarrollo

Detalle que cuesta caro si se ignora: BUZZ_REPLICA_HEAD_MAX_AGE_SECS fue renombrada a BUZZ_REPLICA_READ_MAX_AGE_MS, y no es un alias. Si el relay encuentra el nombre viejo definido, se niega a arrancar con el mensaje BUZZ_REPLICA_HEAD_MAX_AGE_SECS was renamed to BUZZ_REPLICA_READ_MAX_AGE_MS. El motivo está en el comentario del código: honrarla en silencio significaría mil veces el presupuesto previsto.

Sobre BUZZ_AUTO_MIGRATE: en el bundle el default efectivo es false (compose.yml:21 usa ${BUZZ_AUTO_MIGRATE:-false}), pero la plantilla de producción la trae en true. La regla del README del bundle es explícita: o la pones en true, o corres buzz-admin migrate antes de arrancar contra una base de datos nueva. La automigración exige una imagen que incluya las migraciones SQLx embebidas.

En el bundle de Compose, además, DATABASE_URL se construye en compose.yml:12 a partir de POSTGRES_USER, POSTGRES_PASSWORD y POSTGRES_DB, apuntando siempre al host postgres. POSTGRES_PASSWORD es obligatoria: se interpola como ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}, que hace fallar el docker compose completo si falta.


Bloque 2: Redis

VariableDefault del binarioPara qué sirveCuándo tocarla
REDIS_URLredis://localhost:6379Pub/sub de fan-out, presencia, typing, rate limits compartidos, seen-set NIP-98Siempre en producción
BUZZ_REDIS_POOL_SIZE16Conexiones del pool deadpool-redisSegún concurrencia
REDIS_PASSWORDsin default, obligatoria en el bundlePassword del contenedor Redis del bundleSiempre en producción

En el bundle, REDIS_URL también se compone: redis://:${REDIS_PASSWORD}@redis:6379. Y el contenedor arranca con redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}, así que el password es obligatorio en los dos extremos.

Redis no es opcional cuando corres más de un proceso de relay: el pub/sub es lo que hace que un evento publicado contra la instancia A llegue a un cliente suscrito en la instancia B, y el rate limiter compartido vive ahí. Cuando el store de admisión no responde, el relay falla cerrado y rechaza con rate-limited: shared admission unavailable.


Bloque 3: red del relay y URLs públicas

VariableDefault del binarioPara qué sirveCuándo tocarla
BUZZ_BIND_ADDR0.0.0.0:3000Dirección de escucha de WebSocket y RESTRara vez; el bundle la fija
BUZZ_HEALTH_PORT8080Router de salud: /_liveness, /_readiness, /_statusRara vez
BUZZ_METRICS_PORT9102Exporter Prometheus en /metricsRara vez
RELAY_URLws://localhost:3000URL pública que se incrusta en los desafíos NIP-42Siempre en producción
BUZZ_MEDIA_BASE_URLhttp://localhost:3000/mediaBase pública de las URLs de mediaSiempre en producción
BUZZ_CORS_ORIGINSvacíoLista separada por comas de orígenes CORS permitidosSi hay clientes web
BUZZ_DOMAINsolo existe en el .env de producciónHostname público que consumen compose.caddy.yml y el CaddyfileSi usas TLS con Caddy
BUZZ_UDS_PATHsin definirSocket unix opcionalOpcional
BUZZ_PAIRING_RELAY_URLsin definirURL del relay de pairing anunciada en NIP-11; debe ser ws:// o wss://Opcional
BUZZ_WEB_DIR/srv/buzz/web en la imagen DockerDirectorio del bundle web servido en /Rara vez
BUZZ_ADMIN_WEB_DIR/srv/buzz/admin-web en la imagenBundle del dashboard de administraciónRara vez
BUZZ_ADMIN_HOSTsin definirAuthority exacta que activa el dashboard admin de solo lecturaSi quieres el dashboard
BUZZ_SERVE_GIT_WEB_GUIfalseRutas del navegador de repositorios en la UI webOpt-in

RELAY_URL no es cosmética: es la URL que el relay pone dentro del challenge NIP-42. Un cliente que se conecta a wss://buzz.example.com y recibe un challenge que dice ws://localhost:3000 tiene todo el derecho de rechazar la autenticación.

BUZZ_ADMIN_HOST tiene una validación estricta: debe ser una authority exacta, sin /, \ ni @. Si contiene alguno de esos caracteres, el relay no arranca. Y sin ella definida, el bundle admin que la imagen trae en /srv/buzz/admin-web queda inerte: la ruta no existe.

Detalle verificable de la plantilla de producción: BUZZ_MEDIA_SERVER_DOMAIN aparece en deploy/compose/.env.example:12, pero ningún crate del workspace la lee. La variable que gobierna las URLs públicas de media es BUZZ_MEDIA_BASE_URL.


Bloque 4: autenticación, membresía y política

Este es el bloque donde los defaults de desarrollo y los de producción divergen más.

VariableDefault del binarioValor en la plantilla de producciónPara qué sirve
BUZZ_REQUIRE_AUTH_TOKENfalsetrueExige token en la API REST
BUZZ_REQUIRE_RELAY_MEMBERSHIPfalsetrueRelay cerrado: solo miembros publican
BUZZ_ALLOW_NIP_OA_AUTHfalsetruePermite que un agente autentique con la atestación de su dueño
BUZZ_PUBKEY_ALLOWLISTfalseno apareceAllowlist para autenticación NIP-42 solo con pubkey
RELAY_OWNER_PUBKEYsin definirCHANGE_ME_OWNER_PUBKEY_HEXPubkey hex de 64 caracteres del operador
RELAY_OPERATOR_API_ORIGINsin definirno apareceOrigen HTTP de la API de operador
RELAY_OPERATOR_PUBKEYSvacíono aparecePubkeys de operador, separadas por coma
BUZZ_RELAY_PRIVATE_KEYse genera una aleatoriaCHANGE_ME_64_HEX_PRIVATE_KEYClave de firma del relay: su identidad

Cuatro cosas que hay que interiorizar:

RELAY_OWNER_PUBKEY no lleva prefijo BUZZ_, y es deliberado. El comentario en config.rs:631 lo explica: es configuración de identidad del relay que puede compartirse con otros servicios, como el agente ACP. Si el valor no es exactamente 64 caracteres hex, el relay avisa y la ignora en vez de fallar.

RELAY_OPERATOR_PUBKEYS se comporta al revés. Una entrada inválida es un error fatal de configuración, no un aviso: descartar en silencio la pubkey de un operador desactivaría el aprovisionamiento para esa persona sin que nadie se entere. Además, si defines RELAY_OPERATOR_PUBKEYS sin RELAY_OPERATOR_API_ORIGIN, el relay se niega a arrancar; y ese origen debe ser http/https limpio, sin credenciales, path, query ni fragmento.

Si no fijas BUZZ_RELAY_PRIVATE_KEY, el relay firma con una clave distinta en cada arranque. Eso rompe cualquier cosa que dependa de la identidad estable del relay, entre otras el anuncio condicional de NIP-43 en el documento NIP-11.

El aviso de arranque existe y hay que leerlo. Cuando BUZZ_REQUIRE_AUTH_TOKEN es falso, config.rs emite literalmente: “BUZZ_REQUIRE_AUTH_TOKEN is false — REST API requests bypass token auth. WebSocket protocol auth is unaffected. Set to true for production.” La segunda frase importa: la autenticación del protocolo WebSocket no depende de este toggle. Los handlers de REQ, EVENT y COUNT rechazan incondicionalmente cualquier conexión que no esté en estado autenticado.

Política de ingreso opcional, las tres del final de .env.example: BUZZ_TERMS_OF_SERVICE_MARKDOWN, BUZZ_PRIVACY_POLICY_MARKDOWN y BUZZ_AGE_ATTESTATION_REQUIRED. Configurar cualquiera de las tres activa la aceptación de política en todas las superficies de ingreso. Cada documento tiene un tope de 256 KiB; pasarse es error fatal. El relay calcula una versión SHA-256 sobre los tres campos, de modo que cambiar el texto invalida las aceptaciones previas.

Todo el detalle de NIP-42, NIP-98 y la allowlist está en el capítulo 7; membresía y roles, en el capítulo 8.


Bloque 5: límites de conexión, admisión y rate limits

VariableDefaultPara qué sirve
BUZZ_MAX_CONNECTIONS10000Conexiones WebSocket simultáneas; el semáforo se toma antes de leer un solo byte
BUZZ_MAX_CONCURRENT_HANDLERS1024Handlers EVENT/REQ concurrentes en todas las conexiones
BUZZ_SEND_BUFFER1000Buffer de envío por socket
BUZZ_MAX_FRAME_BYTES524288, o sea 512 KiBTamaño máximo de frame WebSocket
BUZZ_SLOW_CLIENT_GRACE_LIMIT15Eventos consecutivos con buffer lleno antes de cortar al cliente lento
BUZZ_DRAIN_JITTER_MS0Jitter de cierre ante SIGTERM; se topa en 20000

Rate limits compartidos en Redis, todos con la familia de parseo estricta de enteros positivos:

VariableDefault
BUZZ_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN60
BUZZ_RATE_LIMIT_HUMAN_API_CALLS_PER_MIN300
BUZZ_RATE_LIMIT_HUMAN_WS_EVENTS_PER_SEC10
BUZZ_RATE_LIMIT_AGENT_STANDARD_MESSAGES_PER_MIN120
BUZZ_RATE_LIMIT_AGENT_STANDARD_API_CALLS_PER_MIN600
BUZZ_RATE_LIMIT_AGENT_ELEVATED_MESSAGES_PER_MIN300
BUZZ_RATE_LIMIT_AGENT_PLATFORM_MESSAGES_PER_MIN600

Poner cualquiera de estas en 0 no las desactiva: hace fallar el arranque con <VAR> must be a positive integer. El tier que se aplica a cada conexión depende de si el AuthContext trae agent_owner_pubkey: con dueño, tier de agente; sin dueño, tier humano. BUZZ_DRAIN_JITTER_MS acepta explícitamente la cadena vacía como “apagado”, así que BUZZ_DRAIN_JITTER_MS= es un kill switch válido y no un crashloop.


Bloque 6: almacenamiento de objetos, media y Blossom

VariableDefault del binarioPara qué sirveCuándo tocarla
BUZZ_S3_ENDPOINThttp://localhost:9000Endpoint S3En Helm sí; en el bundle Compose está fijado
BUZZ_S3_ACCESS_KEYbuzz_devAccess keySiempre en producción
BUZZ_S3_SECRET_KEYbuzz_dev_secretSecret keySiempre en producción
BUZZ_S3_BUCKETbuzz-mediaBucket de media y objetos gitOpcional
BUZZ_S3_REGIONBUZZ_S3_REGION, si falta AWS_REGION, si falta us-east-1Región para la firma SigV4Según proveedor
BUZZ_S3_ADDRESSING_STYLEel default del tipo S3AddressingStylepath o virtual, nada másSegún proveedor

El único valor aceptado además de path es virtual. Cualquier otra cosa es un error fatal con el mensaje BUZZ_S3_ADDRESSING_STYLE must be valid Unicode and one of 'path' or 'virtual'. path produce https://endpoint/bucket/key; virtual produce https://bucket.endpoint/key.

El bundle de Compose fija BUZZ_S3_ENDPOINT=http://minio:9000 y BUZZ_S3_ADDRESSING_STYLE=path en el bloque environment: con el comentario explícito de que el DNS de Docker resuelve minio, no <bucket>.minio. Si necesitas un proveedor S3 externo que exija addressing virtual, el bundle no te sirve tal cual: hay que usar el chart de Helm o una configuración Compose propia. El README del bundle nombra el caso concreto: los buckets nuevos de Railway Storage.

Límites de tamaño: BUZZ_MAX_IMAGE_BYTES 50 MiB, BUZZ_MAX_GIF_BYTES 10 MiB, BUZZ_MAX_VIDEO_BYTES 500 MiB, BUZZ_MAX_FILE_BYTES 100 MiB.

Admisión de subidas:

VariableDefaultPara qué sirve
BUZZ_MEDIA_MAX_CONCURRENT_UPLOADS8Subidas concurrentes por proceso
BUZZ_MEDIA_MAX_CONCURRENT_UPLOADS_PER_PUBKEY2Subidas concurrentes por pubkey; se topa al valor anterior
BUZZ_MEDIA_UPLOADS_PER_MINUTE30Inicios de subida por pubkey y por minuto
BUZZ_MEDIA_UPLOAD_RECORDSfalseRegistros por evento de subida, el canal lateral de moderación
BUZZ_MEDIA_UPLOAD_IP_HEADERvacíoCabecera del edge de confianza con la IP del cliente
BUZZ_MEDIA_UPLOAD_PORT_HEADERvacíoCabecera con el puerto del cliente

Dos variables están muertas: BUZZ_REQUIRE_MEDIA_GET_AUTH y BUZZ_REQUIRE_MEDIA_READ_AUTH ya no se leen. GET y HEAD sobre /media/* siempre exigen auth Blossom t=get más membresía en el relay. Si las defines, el relay avisa al arrancar: “Remove it; a value of false does not re-open unauthenticated media reads.” El capítulo 9 cubre medios, Blossom y git sobre object storage en detalle.

Git sobre object storage

Estas variables comparten el bucket con media, así que las dejo aquí en forma resumida:

VariableDefaultPara qué sirve
BUZZ_GIT_REPO_PATH./repos, /data/git en el bundleRaíz de workspaces git efímeros y caché de packs
BUZZ_GIT_MAX_PACK_BYTES500 MiBTamaño máximo de pack
BUZZ_GIT_MAX_REPO_BYTESel doble del pack, o sea 1 GiB con defaultsTamaño máximo de repositorio
BUZZ_GIT_PACK_CACHE_PATH<repo_path>/.pack-cacheCaché local al proceso
BUZZ_GIT_PACK_CACHE_MAX_BYTEScinco veces MAX_REPO_BYTESTamaño de la caché; 0 desactiva retención
BUZZ_GIT_PACK_CACHE_MAX_CONCURRENT_POPULATIONS2Poblaciones concurrentes de caché
BUZZ_GIT_MAX_REPOS_PER_PUBKEY100Cuota de repositorios por pubkey
BUZZ_GIT_MAX_CONCURRENT_OPS20Operaciones git concurrentes
BUZZ_GIT_HOOK_HMAC_SECRETse genera aleatoria de 64 hexSecreto HMAC de los hooks git
BUZZ_GIT_CONFORMANCE_PROBEtrue salvo el literal falseSonda de escritura condicional contra S3 al arrancar

Dos trampas: si defines BUZZ_GIT_HOOK_HMAC_SECRET con menos de 32 caracteres, el relay no arranca. Y BUZZ_GIT_CONFORMANCE_PROBE está encendida por defecto: si el object store no soporta escrituras condicionales linealizables, el relay falla y no abre el listener.


Bloque 7: búsqueda

.env.example:41-45 trae un bloque titulado “Typesense (search)” con TYPESENSE_API_KEY y TYPESENSE_URL.

Ningún crate del workspace lee esas dos variables. No hay servicio Typesense en docker-compose.yml, ni en el bundle de producción, ni en el chart de Helm. La búsqueda full-text de Buzz corre dentro de Postgres: crates/buzz-search/src/lib.rs documenta que el índice vive en la propia tabla events, como una columna search_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('simple', content)) STORED con acceso por índice GIN. Como la columna es GENERATED ALWAYS, cada escritura de fila es la actualización del índice: no hay indexador separado, ni cola, ni job de reindex, ni ventana de consistencia.

El README del chart de Helm lo confirma desde el otro lado: “Full-text search already runs in Postgres, so no separate search service is provisioned.” Conclusión operativa: no aprovisiones Typesense. El capítulo 10 desarrolla cómo funciona realmente la búsqueda.


Bloque 8: auditoría

VariableDefaultPara qué sirve
BUZZ_AUDIT_ENABLEDtrueLog de auditoría de eventos y media, con encadenamiento SHA-256

Es la única variable de este bloque, no aparece en ninguna de las dos plantillas, y usa el parseo booleano estricto. La documentación del campo en config.rs:230-233 aclara el alcance: no controla el rastro separado de moderation_actions. Para desactivarla hay que escribir BUZZ_AUDIT_ENABLED=false de forma explícita.


Bloque 9: observabilidad

VariableDefaultPara qué sirve
RUST_LOGdev: buzz_relay=debug,...; prod: buzz_relay=info,...Filtro de logs
BUZZ_OTEL_FILTERsin definirFiltro de targets solo para OpenTelemetry
OTEL_EXPORTER_OTLP_ENDPOINTsin definir, es decir trazas apagadasEndpoint OTLP
OTEL_SERVICE_NAMEbuzz-relayNombre de servicio en las trazas
BUZZ_STORAGE_METRICSencendido salvo el literal offKill switch del barrido de almacenamiento
BUZZ_STORAGE_SWEEP_INTERVAL_SECS3600, con piso de 60Frecuencia del barrido
BUZZ_STORAGE_SWEEP_TIMEOUT_SECS120Timeout del barrido
BUZZ_STORAGE_SWEEP_MAX_OBJECTS1000000Tope acumulado de objetos listados
BUZZ_USAGE_METRICS_INTERVAL_SECS300, con piso de 5Intervalo del poller de uso
BUZZ_USAGE_METRICS_IDLE_TIMEOUT_SECS900, nunca menos de tres intervalosVida de los gauges antes de expirar
BUZZ_USAGE_METRICS_PER_COMMUNITYallall o off; controla las series por comunidad
BUZZ_POOL_METRICS_INTERVAL_SECS10, con piso de 1Poller de estadísticas de pools de DB y Redis

BUZZ_OTEL_FILTER existe por una razón concreta que el comentario de .env.example explica: está deliberadamente separada de RUST_LOG para que cambiar la verbosidad de los logs no pueda romper la parentela de trazas.

BUZZ_USAGE_METRICS_PER_COMMUNITY es la palanca de costo en despliegues multi-tenant: el comentario en main.rs estima unas 25 combinaciones de etiquetas de gauge por comunidad, y advierte que un relay con miles de comunidades incurriría en costos de cinco cifras mensuales si todas emiten siempre el set completo. Un valor desconocido no falla: avisa y vuelve a all. BUZZ_STORAGE_METRICS=off apaga el barrido completo y deja de emitir toda la familia de gauges de almacenamiento, incluidos los de salud: está pensado para despliegues cuyo rol IAM no tiene s3:ListBucket.

Todo esto se aterriza en el capítulo 12.


Bloque 10: multi-tenant y ciclo de vida de comunidades

VariableDefaultPara qué sirve
BUZZ_MAX_COMMUNITIES_PER_OWNER5Tope de comunidades por dueño; se lee una vez y se cachea por proceso
BUZZ_COMMUNITY_REVALIDATE_INTERVAL_SECS30, acotado a 1–300Revalidación periódica de comunidades con sockets vivos
BUZZ_NIP43_RECONCILE_INTERVAL_SECS60, con piso de 1Reconciliación de los snapshots de membresía NIP-43
BUZZ_RECONCILE_CHANNELSsin definirEmite eventos kind 39000 y 39002 faltantes al arrancar
BUZZ_EPHEMERAL_TTL_OVERRIDEsin definirFuerza el TTL de todos los canales efímeros, en segundos
BUZZ_REAPER_INTERVAL_SECS60Frecuencia del reaper de canales expirados

BUZZ_MAX_COMMUNITIES_PER_OWNER se cachea en un OnceLock para toda la vida del proceso: no es recargable en caliente, y un valor ausente, no parseable o no positivo cae al default de 5. BUZZ_EPHEMERAL_TTL_OVERRIDE emite un aviso explícito al arrancar, porque pisa el TTL que pidió el cliente: está pensada para probar la expiración rápido, no para producción. El capítulo 11 profundiza en el modelo multi-tenant.


Bloque 11: agentes, el harness ACP

Aquí hay que ser preciso: estas variables no configuran el relay. Configuran buzz-acp, el harness que conecta agentes IA al relay. Ocupan más de cien líneas de .env.example porque cada una mapea a un flag CLI homónimo.

La distinción que más confusión causa:

VariableQuién la leeQué significa
RELAY_URLel relayURL pública propia, la que va en el challenge NIP-42
BUZZ_RELAY_URLel harness ACPRelay al que el harness se conecta

En desarrollo local las dos apuntan al mismo sitio, y por eso se confunden. El propio .env.example lo aclara en un comentario.

Las principales, todas opcionales salvo la primera:

VariableDefaultPara qué sirve
BUZZ_PRIVATE_KEYsin default, requeridaClave privada Nostr del agente, hex o nsec1…
BUZZ_RELAY_URLws://localhost:3000Relay destino
BUZZ_ACP_AGENT_COMMANDgooseBinario a lanzar como agente
BUZZ_ACP_AGENT_ARGSacp para Goose, vacío para Codex y ClaudeArgumentos del binario
BUZZ_ACP_MCP_COMMANDvacíoSidecar MCP opcional
BUZZ_ACP_AGENTS1, rango 1–32Subprocesos paralelos
BUZZ_ACP_MODELvacíoID de modelo; buzz-acp models lista los disponibles
BUZZ_ACP_TURN_TIMEOUT320 segundosTimeout por turno
BUZZ_ACP_MAX_TURNS_PER_SESSION0, desactivadoRotación proactiva; se recomienda 50 en agentes de larga vida
BUZZ_ACP_HEARTBEAT_INTERVAL0, desactivado; debe ser 0 o al menos 10Se recomienda 60 en agentes de larga vida
BUZZ_ACP_SUBSCRIBEmentionsTambién all o config
BUZZ_ACP_KINDS / BUZZ_ACP_CHANNELSvacíoKinds y UUIDs de canal para acotar el alcance
BUZZ_ACP_CONFIG./buzz-acp.tomlTOML de reglas para el modo config
BUZZ_ACP_DEDUPqueueO drop
BUZZ_ACP_CONTEXT_MESSAGE_LIMIT12, rango 0–100Mensajes de contexto para hilos y DMs
BUZZ_ACP_EVENT_BUFFER256, mínimo 1Buffer del canal de eventos

BUZZ_ACP_PRIVATE_KEY sigue aceptándose como alias legado de BUZZ_PRIVATE_KEY, pero el nombre canónico es el corto.


Bloque 12: workflows

El motor de workflows no se configura por variables de entorno propias. Sus definiciones son YAML almacenado como eventos, y su ciclo de vida vive en la base de datos. Solo hay tres variables, y las lee la acción AddReaction del ejecutor cuando llama de vuelta a la API REST del relay:

VariableDefaultPara qué sirve
BUZZ_RELAY_BASE_URLhttp://localhost:3000Base HTTP a la que el ejecutor hace las llamadas de vuelta
BUZZ_API_TOKENsin definirSe envía como Authorization: Bearer si está presente
BUZZ_RELAY_PUBKEYsin definirFallback: se envía como cabecera X-Pubkey si no hay token

El README del repositorio ubica el estado de esta área con precisión: los workflows YAML con triggers de mensaje, reacción, schedule y webhook están en la columna “Works today”, mientras que las compuertas de aprobación están en “Being wired up”, con la nota de que la infraestructura existe pero el pegamento todavía se está secando.


Bloque 13: feature flags

Es la pregunta que suele aparecer al ver preview-features.json en la raíz del repositorio: ¿es configuración del relay? No. Es un manifiesto del cliente de escritorio.

Trae version: 1 y cinco features, todas con platforms: ["desktop"]: workflows, projects, pulse, forum y agentManagedProfiles.

El archivo se importa como alias @features-manifest desde desktop/vite.config.ts y se valida contra un esquema en tiempo de arranque de la app. La semántica está documentada en desktop/src/shared/features/useFeatureEnabled.ts:

  • Si el id está en el manifiesto es una feature preview: se resuelve primero el override explícito del usuario y, si no hay, el default del manifiesto.
  • Si el id no está en el manifiesto, la feature se considera estable y siempre devuelve true. Es un fail-open deliberado: un FeatureGate que apunta a un id borrado nunca esconderá interfaz.

Es decir: la pertenencia al manifiesto significa “esto necesita compuerta”, no “esto existe”. Como operador de relay este archivo no te afecta: los overrides viven en el localStorage del cliente, no en el servidor.


Checklist: lo que cambia sí o sí en producción

flowchart LR
    A["Identidad"] --> A1["BUZZ_RELAY_PRIVATE_KEY"]
    A --> A2["RELAY_OWNER_PUBKEY"]
    B["URLs publicas"] --> B1["RELAY_URL"]
    B --> B2["BUZZ_MEDIA_BASE_URL"]
    B --> B3["BUZZ_CORS_ORIGINS"]
    C["Secretos de infra"] --> C1["POSTGRES_PASSWORD"]
    C --> C2["REDIS_PASSWORD"]
    C --> C3["BUZZ_S3_ACCESS_KEY y BUZZ_S3_SECRET_KEY"]
    C --> C4["BUZZ_GIT_HOOK_HMAC_SECRET"]
    D["Politica"] --> D1["BUZZ_REQUIRE_AUTH_TOKEN a true"]
    D --> D2["BUZZ_REQUIRE_RELAY_MEMBERSHIP a true"]
    E["Operacion"] --> E1["BUZZ_IMAGE pinneada"]
    E --> E2["BUZZ_AUTO_MIGRATE decidida"]
    E --> E3["RUST_LOG a info"]

Los secretos del grupo C más BUZZ_RELAY_PRIVATE_KEY deben ser estables entre reinicios y estar respaldados. La checklist que imprime ./run.sh backup-hint empieza precisamente por el archivo .env. Sobre BUZZ_IMAGE: el default es ghcr.io/block/buzz:main, pensado para pruebas tempranas; el README del bundle es claro en que para producción hay que pinnear ghcr.io/block/buzz:sha-<7> o un tag semver cuando exista.

Variables que hacen fallar el arranque

Vale la pena tener esta lista a mano, porque son fallos de configuración explícitos, no crashes misteriosos:

CondiciónResultado
BUZZ_REPLICA_HEAD_MAX_AGE_SECS definidaError: fue renombrada a BUZZ_REPLICA_READ_MAX_AGE_MS
BUZZ_S3_ADDRESSING_STYLE distinta de path o virtualError de valor inválido
BUZZ_GIT_HOOK_HMAC_SECRET con menos de 32 caracteresError: mínimo 32 caracteres
Cualquier BUZZ_RATE_LIMIT_* en cero o no numéricaError: debe ser entero positivo
BUZZ_ADMIN_HOST con /, \ o @Error: debe ser una authority exacta
BUZZ_ADMIN_WEB_DIR o BUZZ_WEB_DIR sin index.html dentroError: el directorio no contiene index.html
RELAY_OPERATOR_PUBKEYS con una entrada no hex de 64 caracteresError de valor inválido
RELAY_OPERATOR_PUBKEYS sin RELAY_OPERATOR_API_ORIGINError: el origen es requerido
BUZZ_PUSH_GATEWAY_TIMEOUT_MS fuera de 100 a 10000Error de rango
BUZZ_TERMS_OF_SERVICE_MARKDOWN de más de 256 KiBError de tamaño
Sonda de conformidad git fallida con BUZZ_GIT_CONFORMANCE_PROBE=trueEl relay no abre el listener

En cambio, RELAY_OWNER_PUBKEY inválida solo produce un aviso y se ignora. Es la asimetría más fácil de pasar por alto: puedes creer que configuraste un dueño y no haberlo hecho.

Resumen

  • La configuración de Buzz es solo entorno. En desarrollo la carga just gracias a set dotenv-load := true; en el bundle de producción la carga Compose vía env_file, con el bloque environment: pisando lo que venga del .env.
  • Existen dos plantillas: .env.example en la raíz para desarrollo, con defaults que apuntan a los contenedores locales, y deploy/compose/.env.example para producción, donde todo secreto es un literal CHANGE_ME que run.sh se niega a ejecutar sin reemplazar.
  • Los defaults del binario no siempre coinciden con la plantilla. BUZZ_REQUIRE_AUTH_TOKEN, BUZZ_REQUIRE_RELAY_MEMBERSHIP y BUZZ_ALLOW_NIP_OA_AUTH son false en el código y true en la plantilla de producción. Además, algunos booleanos aceptan solo true o 1 exactos y tratan cualquier otra cosa como falso sin avisar, mientras otros fallan el arranque.
  • RELAY_OWNER_PUBKEY, RELAY_OPERATOR_PUBKEYS y RELAY_OPERATOR_API_ORIGIN no llevan prefijo BUZZ_ a propósito. La primera avisa e ignora si es inválida; la segunda es error fatal.
  • El bloque Typesense de .env.example es residual: ningún crate lee TYPESENSE_URL ni TYPESENSE_API_KEY. La búsqueda full-text corre dentro de Postgres con una columna GENERATED ALWAYS.
  • BUZZ_REQUIRE_MEDIA_GET_AUTH y BUZZ_REQUIRE_MEDIA_READ_AUTH están muertas: las lecturas de media siempre exigen auth Blossom y membresía. Las variables BUZZ_ACP_* configuran el harness de agentes, no el relay: ojo con RELAY_URL frente a BUZZ_RELAY_URL.
  • preview-features.json es un manifiesto del cliente de escritorio con semántica fail-open, no configuración del servidor.

Siguiente: Despliegue en un VPS con Docker Compose