Configuración: referencia de variables de entorno
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:
- En desarrollo,
Justfile:3declaraset dotenv-load := true. Todas las recetas dejustcargan.envantes de ejecutar nada. No necesitassource .env. - En el bundle de Compose de producción, el servicio
relayusaenv_file: [.env]y además un bloqueenvironment:explícito. Las claves del bloqueenvironment:ganan sobre las del.env. Ahí es dondedeploy/compose/compose.ymlfijaBUZZ_BIND_ADDR,BUZZ_HEALTH_PORT,BUZZ_METRICS_PORT,DATABASE_URL,REDIS_URL,BUZZ_S3_ENDPOINT,BUZZ_S3_ADDRESSING_STYLEyBUZZ_GIT_REPO_PATH. Ponerlas en tu.envno tiene efecto. - 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
| Archivo | Líneas | Público | Filosofía |
|---|---|---|---|
.env.example (raíz) | 252 | Desarrollo local | Todo funciona con docker compose up sin editar nada |
deploy/compose/.env.example | 52 | VPS de producción | Todos 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:
| Familia | Función | Acepta como verdadero | Comportamiento con basura |
|---|---|---|---|
| Booleano estricto | parse_bool en config.rs:394 | true, 1, on | Error fatal de configuración |
| Booleano laxo | comparación inline | solo "true" o "1" exactos | Se interpreta como false, sin aviso |
| Entero positivo | positive_u64_from_env en config.rs:301 | cualquier entero mayor que cero | Error 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
| Variable | Default del binario | Para qué sirve | Cuándo tocarla |
|---|---|---|---|
DATABASE_URL | postgres://buzz:buzz_dev@localhost:5432/buzz | URL del Postgres writer | Siempre en producción |
READ_DATABASE_URL | sin definir | Réplica de lectura opcional; vacío mantiene todas las lecturas en el writer | Solo si tienes réplica |
BUZZ_DB_POOL_SIZE | 50 | Conexiones máximas del pool writer | Según carga y max_connections de Postgres |
BUZZ_DB_READ_POOL_SIZE | sin definir, cae al valor del writer | Pool del reader | Si la réplica tiene otro dimensionamiento |
BUZZ_REPLICA_READ_MAX_AGE_MS | 0, es decir apagado | Frescura máxima tolerada al enrutar lecturas a la réplica, en milisegundos | Al habilitar lecturas en réplica |
BUZZ_AUTO_MIGRATE | desactivado si no se define | Ejecuta las migraciones SQLx embebidas al arrancar | Decisión consciente, ver abajo |
PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE | localhost, 5432, buzz, buzz_dev, buzz | Variables de psql para los scripts de desarrollo | Solo 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
| Variable | Default del binario | Para qué sirve | Cuándo tocarla |
|---|---|---|---|
REDIS_URL | redis://localhost:6379 | Pub/sub de fan-out, presencia, typing, rate limits compartidos, seen-set NIP-98 | Siempre en producción |
BUZZ_REDIS_POOL_SIZE | 16 | Conexiones del pool deadpool-redis | Según concurrencia |
REDIS_PASSWORD | sin default, obligatoria en el bundle | Password del contenedor Redis del bundle | Siempre 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
| Variable | Default del binario | Para qué sirve | Cuándo tocarla |
|---|---|---|---|
BUZZ_BIND_ADDR | 0.0.0.0:3000 | Dirección de escucha de WebSocket y REST | Rara vez; el bundle la fija |
BUZZ_HEALTH_PORT | 8080 | Router de salud: /_liveness, /_readiness, /_status | Rara vez |
BUZZ_METRICS_PORT | 9102 | Exporter Prometheus en /metrics | Rara vez |
RELAY_URL | ws://localhost:3000 | URL pública que se incrusta en los desafíos NIP-42 | Siempre en producción |
BUZZ_MEDIA_BASE_URL | http://localhost:3000/media | Base pública de las URLs de media | Siempre en producción |
BUZZ_CORS_ORIGINS | vacío | Lista separada por comas de orígenes CORS permitidos | Si hay clientes web |
BUZZ_DOMAIN | solo existe en el .env de producción | Hostname público que consumen compose.caddy.yml y el Caddyfile | Si usas TLS con Caddy |
BUZZ_UDS_PATH | sin definir | Socket unix opcional | Opcional |
BUZZ_PAIRING_RELAY_URL | sin definir | URL del relay de pairing anunciada en NIP-11; debe ser ws:// o wss:// | Opcional |
BUZZ_WEB_DIR | /srv/buzz/web en la imagen Docker | Directorio del bundle web servido en / | Rara vez |
BUZZ_ADMIN_WEB_DIR | /srv/buzz/admin-web en la imagen | Bundle del dashboard de administración | Rara vez |
BUZZ_ADMIN_HOST | sin definir | Authority exacta que activa el dashboard admin de solo lectura | Si quieres el dashboard |
BUZZ_SERVE_GIT_WEB_GUI | false | Rutas del navegador de repositorios en la UI web | Opt-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.
| Variable | Default del binario | Valor en la plantilla de producción | Para qué sirve |
|---|---|---|---|
BUZZ_REQUIRE_AUTH_TOKEN | false | true | Exige token en la API REST |
BUZZ_REQUIRE_RELAY_MEMBERSHIP | false | true | Relay cerrado: solo miembros publican |
BUZZ_ALLOW_NIP_OA_AUTH | false | true | Permite que un agente autentique con la atestación de su dueño |
BUZZ_PUBKEY_ALLOWLIST | false | no aparece | Allowlist para autenticación NIP-42 solo con pubkey |
RELAY_OWNER_PUBKEY | sin definir | CHANGE_ME_OWNER_PUBKEY_HEX | Pubkey hex de 64 caracteres del operador |
RELAY_OPERATOR_API_ORIGIN | sin definir | no aparece | Origen HTTP de la API de operador |
RELAY_OPERATOR_PUBKEYS | vacío | no aparece | Pubkeys de operador, separadas por coma |
BUZZ_RELAY_PRIVATE_KEY | se genera una aleatoria | CHANGE_ME_64_HEX_PRIVATE_KEY | Clave 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
| Variable | Default | Para qué sirve |
|---|---|---|
BUZZ_MAX_CONNECTIONS | 10000 | Conexiones WebSocket simultáneas; el semáforo se toma antes de leer un solo byte |
BUZZ_MAX_CONCURRENT_HANDLERS | 1024 | Handlers EVENT/REQ concurrentes en todas las conexiones |
BUZZ_SEND_BUFFER | 1000 | Buffer de envío por socket |
BUZZ_MAX_FRAME_BYTES | 524288, o sea 512 KiB | Tamaño máximo de frame WebSocket |
BUZZ_SLOW_CLIENT_GRACE_LIMIT | 15 | Eventos consecutivos con buffer lleno antes de cortar al cliente lento |
BUZZ_DRAIN_JITTER_MS | 0 | Jitter de cierre ante SIGTERM; se topa en 20000 |
Rate limits compartidos en Redis, todos con la familia de parseo estricta de enteros positivos:
| Variable | Default |
|---|---|
BUZZ_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN | 60 |
BUZZ_RATE_LIMIT_HUMAN_API_CALLS_PER_MIN | 300 |
BUZZ_RATE_LIMIT_HUMAN_WS_EVENTS_PER_SEC | 10 |
BUZZ_RATE_LIMIT_AGENT_STANDARD_MESSAGES_PER_MIN | 120 |
BUZZ_RATE_LIMIT_AGENT_STANDARD_API_CALLS_PER_MIN | 600 |
BUZZ_RATE_LIMIT_AGENT_ELEVATED_MESSAGES_PER_MIN | 300 |
BUZZ_RATE_LIMIT_AGENT_PLATFORM_MESSAGES_PER_MIN | 600 |
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
| Variable | Default del binario | Para qué sirve | Cuándo tocarla |
|---|---|---|---|
BUZZ_S3_ENDPOINT | http://localhost:9000 | Endpoint S3 | En Helm sí; en el bundle Compose está fijado |
BUZZ_S3_ACCESS_KEY | buzz_dev | Access key | Siempre en producción |
BUZZ_S3_SECRET_KEY | buzz_dev_secret | Secret key | Siempre en producción |
BUZZ_S3_BUCKET | buzz-media | Bucket de media y objetos git | Opcional |
BUZZ_S3_REGION | BUZZ_S3_REGION, si falta AWS_REGION, si falta us-east-1 | Región para la firma SigV4 | Según proveedor |
BUZZ_S3_ADDRESSING_STYLE | el default del tipo S3AddressingStyle | path o virtual, nada más | Segú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:
| Variable | Default | Para qué sirve |
|---|---|---|
BUZZ_MEDIA_MAX_CONCURRENT_UPLOADS | 8 | Subidas concurrentes por proceso |
BUZZ_MEDIA_MAX_CONCURRENT_UPLOADS_PER_PUBKEY | 2 | Subidas concurrentes por pubkey; se topa al valor anterior |
BUZZ_MEDIA_UPLOADS_PER_MINUTE | 30 | Inicios de subida por pubkey y por minuto |
BUZZ_MEDIA_UPLOAD_RECORDS | false | Registros por evento de subida, el canal lateral de moderación |
BUZZ_MEDIA_UPLOAD_IP_HEADER | vacío | Cabecera del edge de confianza con la IP del cliente |
BUZZ_MEDIA_UPLOAD_PORT_HEADER | vacío | Cabecera 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:
| Variable | Default | Para qué sirve |
|---|---|---|
BUZZ_GIT_REPO_PATH | ./repos, /data/git en el bundle | Raíz de workspaces git efímeros y caché de packs |
BUZZ_GIT_MAX_PACK_BYTES | 500 MiB | Tamaño máximo de pack |
BUZZ_GIT_MAX_REPO_BYTES | el doble del pack, o sea 1 GiB con defaults | Tamaño máximo de repositorio |
BUZZ_GIT_PACK_CACHE_PATH | <repo_path>/.pack-cache | Caché local al proceso |
BUZZ_GIT_PACK_CACHE_MAX_BYTES | cinco veces MAX_REPO_BYTES | Tamaño de la caché; 0 desactiva retención |
BUZZ_GIT_PACK_CACHE_MAX_CONCURRENT_POPULATIONS | 2 | Poblaciones concurrentes de caché |
BUZZ_GIT_MAX_REPOS_PER_PUBKEY | 100 | Cuota de repositorios por pubkey |
BUZZ_GIT_MAX_CONCURRENT_OPS | 20 | Operaciones git concurrentes |
BUZZ_GIT_HOOK_HMAC_SECRET | se genera aleatoria de 64 hex | Secreto HMAC de los hooks git |
BUZZ_GIT_CONFORMANCE_PROBE | true salvo el literal false | Sonda 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
| Variable | Default | Para qué sirve |
|---|---|---|
BUZZ_AUDIT_ENABLED | true | Log 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
| Variable | Default | Para qué sirve |
|---|---|---|
RUST_LOG | dev: buzz_relay=debug,...; prod: buzz_relay=info,... | Filtro de logs |
BUZZ_OTEL_FILTER | sin definir | Filtro de targets solo para OpenTelemetry |
OTEL_EXPORTER_OTLP_ENDPOINT | sin definir, es decir trazas apagadas | Endpoint OTLP |
OTEL_SERVICE_NAME | buzz-relay | Nombre de servicio en las trazas |
BUZZ_STORAGE_METRICS | encendido salvo el literal off | Kill switch del barrido de almacenamiento |
BUZZ_STORAGE_SWEEP_INTERVAL_SECS | 3600, con piso de 60 | Frecuencia del barrido |
BUZZ_STORAGE_SWEEP_TIMEOUT_SECS | 120 | Timeout del barrido |
BUZZ_STORAGE_SWEEP_MAX_OBJECTS | 1000000 | Tope acumulado de objetos listados |
BUZZ_USAGE_METRICS_INTERVAL_SECS | 300, con piso de 5 | Intervalo del poller de uso |
BUZZ_USAGE_METRICS_IDLE_TIMEOUT_SECS | 900, nunca menos de tres intervalos | Vida de los gauges antes de expirar |
BUZZ_USAGE_METRICS_PER_COMMUNITY | all | all o off; controla las series por comunidad |
BUZZ_POOL_METRICS_INTERVAL_SECS | 10, con piso de 1 | Poller 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
| Variable | Default | Para qué sirve |
|---|---|---|
BUZZ_MAX_COMMUNITIES_PER_OWNER | 5 | Tope de comunidades por dueño; se lee una vez y se cachea por proceso |
BUZZ_COMMUNITY_REVALIDATE_INTERVAL_SECS | 30, acotado a 1–300 | Revalidación periódica de comunidades con sockets vivos |
BUZZ_NIP43_RECONCILE_INTERVAL_SECS | 60, con piso de 1 | Reconciliación de los snapshots de membresía NIP-43 |
BUZZ_RECONCILE_CHANNELS | sin definir | Emite eventos kind 39000 y 39002 faltantes al arrancar |
BUZZ_EPHEMERAL_TTL_OVERRIDE | sin definir | Fuerza el TTL de todos los canales efímeros, en segundos |
BUZZ_REAPER_INTERVAL_SECS | 60 | Frecuencia 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:
| Variable | Quién la lee | Qué significa |
|---|---|---|
RELAY_URL | el relay | URL pública propia, la que va en el challenge NIP-42 |
BUZZ_RELAY_URL | el harness ACP | Relay 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:
| Variable | Default | Para qué sirve |
|---|---|---|
BUZZ_PRIVATE_KEY | sin default, requerida | Clave privada Nostr del agente, hex o nsec1… |
BUZZ_RELAY_URL | ws://localhost:3000 | Relay destino |
BUZZ_ACP_AGENT_COMMAND | goose | Binario a lanzar como agente |
BUZZ_ACP_AGENT_ARGS | acp para Goose, vacío para Codex y Claude | Argumentos del binario |
BUZZ_ACP_MCP_COMMAND | vacío | Sidecar MCP opcional |
BUZZ_ACP_AGENTS | 1, rango 1–32 | Subprocesos paralelos |
BUZZ_ACP_MODEL | vacío | ID de modelo; buzz-acp models lista los disponibles |
BUZZ_ACP_TURN_TIMEOUT | 320 segundos | Timeout por turno |
BUZZ_ACP_MAX_TURNS_PER_SESSION | 0, desactivado | Rotación proactiva; se recomienda 50 en agentes de larga vida |
BUZZ_ACP_HEARTBEAT_INTERVAL | 0, desactivado; debe ser 0 o al menos 10 | Se recomienda 60 en agentes de larga vida |
BUZZ_ACP_SUBSCRIBE | mentions | También all o config |
BUZZ_ACP_KINDS / BUZZ_ACP_CHANNELS | vacío | Kinds y UUIDs de canal para acotar el alcance |
BUZZ_ACP_CONFIG | ./buzz-acp.toml | TOML de reglas para el modo config |
BUZZ_ACP_DEDUP | queue | O drop |
BUZZ_ACP_CONTEXT_MESSAGE_LIMIT | 12, rango 0–100 | Mensajes de contexto para hilos y DMs |
BUZZ_ACP_EVENT_BUFFER | 256, mínimo 1 | Buffer 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:
| Variable | Default | Para qué sirve |
|---|---|---|
BUZZ_RELAY_BASE_URL | http://localhost:3000 | Base HTTP a la que el ejecutor hace las llamadas de vuelta |
BUZZ_API_TOKEN | sin definir | Se envía como Authorization: Bearer si está presente |
BUZZ_RELAY_PUBKEY | sin definir | Fallback: 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: unFeatureGateque 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ón | Resultado |
|---|---|
BUZZ_REPLICA_HEAD_MAX_AGE_SECS definida | Error: fue renombrada a BUZZ_REPLICA_READ_MAX_AGE_MS |
BUZZ_S3_ADDRESSING_STYLE distinta de path o virtual | Error de valor inválido |
BUZZ_GIT_HOOK_HMAC_SECRET con menos de 32 caracteres | Error: mínimo 32 caracteres |
Cualquier BUZZ_RATE_LIMIT_* en cero o no numérica | Error: 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 dentro | Error: el directorio no contiene index.html |
RELAY_OPERATOR_PUBKEYS con una entrada no hex de 64 caracteres | Error de valor inválido |
RELAY_OPERATOR_PUBKEYS sin RELAY_OPERATOR_API_ORIGIN | Error: el origen es requerido |
BUZZ_PUSH_GATEWAY_TIMEOUT_MS fuera de 100 a 10000 | Error de rango |
BUZZ_TERMS_OF_SERVICE_MARKDOWN de más de 256 KiB | Error de tamaño |
Sonda de conformidad git fallida con BUZZ_GIT_CONFORMANCE_PROBE=true | El 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
justgracias aset dotenv-load := true; en el bundle de producción la carga Compose víaenv_file, con el bloqueenvironment:pisando lo que venga del.env. - Existen dos plantillas:
.env.exampleen la raíz para desarrollo, con defaults que apuntan a los contenedores locales, ydeploy/compose/.env.examplepara producción, donde todo secreto es un literalCHANGE_MEquerun.shse niega a ejecutar sin reemplazar. - Los defaults del binario no siempre coinciden con la plantilla.
BUZZ_REQUIRE_AUTH_TOKEN,BUZZ_REQUIRE_RELAY_MEMBERSHIPyBUZZ_ALLOW_NIP_OA_AUTHsonfalseen el código ytrueen la plantilla de producción. Además, algunos booleanos aceptan solotrueo1exactos y tratan cualquier otra cosa como falso sin avisar, mientras otros fallan el arranque. RELAY_OWNER_PUBKEY,RELAY_OPERATOR_PUBKEYSyRELAY_OPERATOR_API_ORIGINno llevan prefijoBUZZ_a propósito. La primera avisa e ignora si es inválida; la segunda es error fatal.- El bloque Typesense de
.env.examplees residual: ningún crate leeTYPESENSE_URLniTYPESENSE_API_KEY. La búsqueda full-text corre dentro de Postgres con una columnaGENERATED ALWAYS. BUZZ_REQUIRE_MEDIA_GET_AUTHyBUZZ_REQUIRE_MEDIA_READ_AUTHestán muertas: las lecturas de media siempre exigen auth Blossom y membresía. Las variablesBUZZ_ACP_*configuran el harness de agentes, no el relay: ojo conRELAY_URLfrente aBUZZ_RELAY_URL.preview-features.jsones un manifiesto del cliente de escritorio con semántica fail-open, no configuración del servidor.
Siguiente: Despliegue en un VPS con Docker Compose