Backups, migraciones y actualizaciones

Por: Artiko
buzznostrbackupmigracionespostgresdockerhelmrelease

Backups, migraciones y actualizaciones

Los capítulos anteriores dejaron el relay corriendo, configurado y observado. Este cubre lo que hace que ese relay sobreviva a un cambio de versión, a un disco perdido y a un error humano: el estado del esquema, el respaldo de cada pieza de estado y el procedimiento de actualización. La idea rectora es simple: Buzz guarda estado en cuatro lugares distintos y ninguno respalda a los otros.

El inventario de estado

Antes de hablar de backups conviene saber exactamente qué hay que respaldar. El propio bundle de producción lo enumera: deploy/compose/run.sh incluye una función backup_hint() que se imprime con ./run.sh backup-hint y también al final de cada ./run.sh upgrade.

flowchart TD
    Relay["Relay buzz"] --> PG["Postgres: eventos, canales, membresia, moderacion"]
    Relay --> S3["MinIO o S3: media y objetos git"]
    Relay --> GIT["Volumen buzz-git-data en BUZZ_GIT_REPO_PATH"]
    Relay --> SEC["Secretos: .env con BUZZ_RELAY_PRIVATE_KEY y HMAC"]
    Relay --> REDIS["Redis: pubsub, presencia, rate limits"]
    REDIS -.->|"efimero, se regenera"| Relay
PiezaDónde viveQué se pierde si falta
PostgresVolumen buzz-postgres-data (Compose) o base gestionadaEl almacén canónico de eventos: mensajes, canales, hilos, membresía, moderación, audit log, invitaciones. Sin esto no hay comunidad.
Bucket S3/MinIOVolumen buzz-minio-data o proveedor externoBlobs de media y objetos git. Los eventos que los referencian siguen en Postgres, pero apuntan a nada.
Volumen gitbuzz-git-data, montado en BUZZ_GIT_REPO_PATH=/data/git (compose.yml:20); en Helm es el PVC git con mountPath: /var/lib/buzz/git (values.yaml:280-287)Estado en disco de repos servidos por el endpoint git.
BUZZ_RELAY_PRIVATE_KEYdeploy/compose/.env o un Secret de KubernetesLa identidad del relay. Rotarla equivale a un relay nuevo: el README del chart lo dice sin rodeos, “rotating it = new identity”.
BUZZ_GIT_HOOK_HMAC_SECRETIdemEl secreto que autentica los hooks de git.
Clave privada del ownerEn manos del operador, no en el chartSin ella no hay quien administre la comunidad. El chart la restaura reinstalando con el mismo ownerPubkey.
RedisVolumen buzz-redis-dataNada crítico: scripts/dev-reset.sh lo describe como “ephemeral and always wiped on restart”.
Volúmenes de Caddybuzz-caddy-data / buzz-caddy-configCertificados TLS emitidos. Se vuelven a emitir, pero cuentan contra los límites de rate de la CA.

La regla que cierra la lista, textual en run.sh: “Keep Postgres + object/git state snapshots from the same maintenance window”. Un dump de Postgres de las 03:00 con un espejo de S3 de las 09:00 produce eventos que referencian blobs inexistentes y blobs huérfanos que ningún evento reclama.

Migraciones

Cómo están construidas

Buzz no usa una herramienta externa de migraciones. Usa sqlx con las migraciones embebidas en el binario. La declaración vive en una sola línea (crates/buzz-db/src/migration.rs:11):

static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../../migrations");

La macro sqlx::migrate! lee la carpeta migrations/ en tiempo de compilación y la incrusta en el ejecutable. Consecuencia práctica para el operador: la imagen ghcr.io/block/buzz:<tag> lleva dentro exactamente las migraciones que existían en ese commit. No hay que copiar archivos .sql al servidor, y tampoco se puede “parchear” una migración sin reconstruir la imagen.

La carpeta tiene hoy 28 archivos, de 0001_initial_schema.sql a 0028_long_reaction_payloads.sql. El 0001 es el esquema multi-tenant consolidado; su cabecera aclara que no es aditivo sobre el esquema single-community anterior y que el cutover legado es un script de operador aparte, no estado de migración de arranque. El resto son incrementos pequeños y específicos: 0004_events_tags_gin.sql añade un índice GIN, 0021_created_at_fence_floor.sql instala el trigger de piso de created_at, y 0028_long_reaction_payloads.sql es una sola sentencia ALTER TABLE reactions ALTER COLUMN emoji TYPE VARCHAR(66), para que un shortcode de emoji personalizado de 64 caracteres quepa con sus dos : envolventes de NIP-25. El control de qué se aplicó vive en la tabla estándar de sqlx, _sqlx_migrations.

Los tres caminos para aplicarlas

flowchart LR
    A["BUZZ_AUTO_MIGRATE habilitado"] --> M["MIGRATOR.run sobre el pool"]
    B["buzz-admin migrate"] --> M
    C["just migrate en desarrollo"] --> B
    M --> D["Tabla _sqlx_migrations actualizada"]
  1. Al arrancar el relay. crates/buzz-relay/src/main.rs:188-198 lee BUZZ_AUTO_MIGRATE y, si está habilitada, llama db.migrate(). Si falla, el arranque aborta con Database migration failed. Si no está habilitada, deja el log Skipping database migrations because BUZZ_AUTO_MIGRATE is not enabled. La función que interpreta la variable acepta true, 1, yes u on (case-insensitive); cualquier otra cosa cuenta como desactivado.
  2. Por CLI. buzz-admin migrate conecta a la base y ejecuta lo mismo (crates/buzz-admin/src/main.rs:139-142), imprimiendo Database migrations complete.
  3. En desarrollo. just migrate (Justfile:699) no tiene cuerpo propio: depende de la receta privada _ensure-migrations (Justfile:193-195), que a su vez espera a que Postgres y Redis estén healthy, corre cargo run -p buzz-admin -- migrate y después ./scripts/seed-local-community.sh. Esa dependencia es la razón de que just relay, just relay-release y just dev migren solos antes de arrancar.

El default de BUZZ_AUTO_MIGRATE depende de dónde estés, y este es un punto donde mucha gente se equivoca:

ContextoValor
El binario, sin variable definidaDesactivado (main.rs:188-198)
deploy/compose/compose.yml:21false si el .env no lo define
deploy/compose/.env.example:19true
Chart Helm, values.yaml:377migrate.autoMigrate: true

El README del bundle Compose lo llama explícitamente “opt-in”: hay que ponerlo en true o correr buzz-admin migrate antes de arrancar contra una base nueva.

Guardas: la migración falla en cerrado

run_migrations() no es solo MIGRATOR.run(). Tiene una guarda antes y otra después:

Antes, reject_legacy_nip_rs_cardinality_ambiguity() consulta SELECT max(version) FROM _sqlx_migrations WHERE success. Si la base está poblada y todavía en 0001-0006, busca filas de kind 30078 con d_tag de forma read-state:<32 hex> cuya cardinalidad de tags d/t sea ambigua. Si encuentra alguna, aborta antes de que sqlx abra su transacción, con el mensaje:

NIP-RS migration blocked: pre-0007 database contains kind-30078 rows with
ambiguous d/t tag cardinality; repair or remove those nonconforming rows before retrying

El motivo está en el comentario del código: la migración 0007 está checksum-frozen y purgaría ese historial de forma irreversible. La guarda existe para que el operador inspeccione y repare esas filas primero.

Después de MIGRATOR.run(), verify_floor_guard_catalog() comprueba que el trigger de piso de created_at introducido por 0021 exista con la forma correcta en la tabla padre events y en cada una de sus particiones. CREATE TABLE ... PARTITION OF clona los triggers del padre, pero una partición adjuntada con ATTACH PARTITION o creada por un camino de código antiguo escaparía silenciosamente a la guarda. Si falta en alguna, la migración falla.

Ese mismo chequeo se vuelve a ejecutar al arrancar en relays que no migran: si la verificación falla, el fence de réplica queda cerrado y todas las lecturas de cursor se sirven desde el writer (Replica fence disabled — floor guard verification failed). Es una degradación de rendimiento ruidosa, no una caída.

Justo después de migrar, el relay llama db.ensure_future_partitions(3) (main.rs:200-203): crea por adelantado las particiones futuras de events. Un fallo ahí se registra pero no aborta el arranque.

Inspeccionar el estado del esquema

No hay un subcomando buzz-admin migrate --status; el inventario se consulta con SQL directo sobre la tabla de sqlx. Con el bundle Compose:

cd deploy/compose
docker compose --env-file .env exec postgres \
  psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
  -c "SELECT version, description, success, installed_on FROM _sqlx_migrations ORDER BY version;"

Para la comprobación rápida de “¿estoy al día?”, basta SELECT max(version) FROM _sqlx_migrations WHERE success; — exactamente el valor que consulta la guarda del propio código. Compáralo con el número del último archivo de migrations/ en el commit de la imagen que corres: si es menor, estás sirviendo contra un esquema viejo.

Ese caso importa especialmente en Kubernetes. El README del chart es explícito: los readiness probes solo verifican conectividad a la base, no frescura del esquema, así que un pod puede aparecer sano contra un esquema sin migrar y fallar bajo carga.

Para saber qué versión del relay está corriendo sin entrar al contenedor, usa el documento NIP-11: el campo version de RelayInfo se rellena con env!("CARGO_PKG_VERSION") del crate buzz-relay (crates/buzz-relay/src/nip11.rs).

curl -fsS -H 'Accept: application/nostr+json' https://buzz.midominio.cl/ | jq '.version, .software'

Migraciones en Kubernetes

Con el chart, migrate.autoMigrate: true es el default y helm upgrade es todo el procedimiento de upgrade. Varias réplicas arrancando a la vez compiten de forma race-safe detrás de un advisory lock de Postgres.

Si prefieres desacoplar migración de servicio, migrate.autoMigrate=false es válido, pero entonces el chart no migra por ti: el operador debe correr buzz-admin migrate (un Pod aparte o un Job one-shot) antes de cada helm install / helm upgrade. El NOTES.txt del chart imprime esa advertencia al instalar.

El values expone migrate.preUpgradeJob.enabled con backoffLimit y activeDeadlineSeconds, pero es una llave reservada: el README lo declara como roadmap del chart y no existe plantilla que la implemente. No la configures esperando que haga algo.

Como red de seguridad de todo esto, just test-unit corre cargo nextest run -p buzz-db --lib: tests puros de parseo que concatenan el SQL de todas las migraciones embebidas en orden de versión y verifican los lints de scoping de tenant. Sin ese gate, un archivo perdido en migrations/ se publicaría en verde.

Backup

La checklist canónica

./run.sh backup-hint imprime esto (deploy/compose/run.sh:38-51):

Back up these before upgrades and on a regular schedule:

- deploy/compose/.env, especially BUZZ_RELAY_PRIVATE_KEY, DB/Redis/S3 secrets, and BUZZ_GIT_HOOK_HMAC_SECRET
- The owner private key if bootstrap generated one for RELAY_OWNER_PUBKEY
- Postgres data (prefer pg_dump or a quiesced volume snapshot)
- MinIO/S3 bucket contents for media and git objects
- buzz-git-data volume (BUZZ_GIT_REPO_PATH=/data/git)
- Caddy data/config volumes if using compose.caddy.yml

Keep Postgres + object/git state snapshots from the same maintenance window.

Nota que backup-hint es uno de los pocos subcomandos de run.sh que no pasa por require_env(): puedes imprimir la checklist aunque el .env todavía tenga CHANGE_ME.

Postgres

El bundle corre postgres:17-alpine (deploy/compose/compose.yml:52), así que pg_dump está dentro del contenedor. La recomendación del propio script es preferir pg_dump o un snapshot de volumen en reposo (quiesced) — no un cp del directorio de datos con el servidor escribiendo.

cd deploy/compose
set -a; . ./.env; set +a

docker compose --env-file .env exec -T postgres \
  pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" --format=custom \
  > "buzz-$(date -u +%Y%m%dT%H%M%SZ).dump"

Restauración sobre una base vacía:

docker compose --env-file .env exec -T postgres \
  pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists \
  < buzz-20260807T030000Z.dump

Después de restaurar, verifica el esquema antes de abrir tráfico: consulta max(version) en _sqlx_migrations y, si el dump venía de una versión anterior a la imagen que vas a levantar, deja que el arranque migre (o corre buzz-admin migrate) antes de exponer el puerto.

MinIO / S3

El bundle ya trae un cliente mc: el servicio minio-init usa la imagen minio/mc y hace mc alias set local http://minio:9000 ..., mc mb --ignore-existing y mc anonymous set none sobre BUZZ_S3_BUCKET (default buzz-media). El mismo cliente sirve para espejar el bucket a un destino externo dentro de la ventana de mantenimiento.

Recuerda que BUZZ_S3_ENDPOINT no es configurable por .env en el bundle: compose.yml:14 lo fija a http://minio:9000 en la sección environment, que gana sobre env_file. Si usas S3 gestionado, el respaldo lo hacen las herramientas del proveedor (versionado de bucket, replicación entre regiones), no este stack.

El bucket contiene dos clases de objetos que hay que respaldar juntas: blobs de media y objetos git. El backend de git sobre object storage hidrata un repo efímero desde S3 en cada request, así que el bucket es la fuente de verdad; el volumen buzz-git-data guarda el estado en disco bajo BUZZ_GIT_REPO_PATH.

Secretos

Los secretos no están en la base ni en el bucket: están en deploy/compose/.env o en un Secret de Kubernetes. Son la pieza más fácil de olvidar y la más cara de perder, porque no se regeneran: se reemplazan por otros distintos y eso cambia la identidad.

  • BUZZ_RELAY_PRIVATE_KEY (deploy/compose/.env.example:27): identidad del relay. El README del chart lo dice sin ambigüedad: rotarla es un relay nuevo, y los peers de federación dejan de reconocerlo. Además es la clave con la que el relay firma sus propios eventos, incluido el roster que vimos en Membresía, roles y moderación.
  • BUZZ_GIT_HOOK_HMAC_SECRET (deploy/compose/.env.example:28).
  • Credenciales de Postgres, Redis y S3.
  • La clave privada del owner, que no vive en el stack. El chart la restaura reinstalando con el mismo ownerPubkey.

El README.md del bundle exige que estos valores sean estables entre reinicios; require_env() en run.sh:19-36 aborta start, restart, pull, upgrade y config si el .env falta o si queda cualquier línea con CHANGE_ME.

En Kubernetes, el NOTES.txt advierte que poner secrets.relayPrivateKey o secrets.gitHookHmacSecret inline en values.yaml filtra secretos al historial de git y a los logs de CI: lo correcto es un Secret externo con secrets.existingSecret. Ese camino es además el único seguro bajo ArgoCD o Flux, porque esas herramientas renderizan con helm template y ahí lookup devuelve vacío, rotando los secretos en cada sync.

Orden de restauración

flowchart TD
    S1["Parar el relay: run.sh stop"] --> S2["Restaurar secretos: .env o Secret"]
    S2 --> S3["Restaurar Postgres: pg_restore"]
    S3 --> S4["Restaurar bucket S3 y volumen git"]
    S4 --> S5["Aplicar migraciones pendientes"]
    S5 --> S6["Arrancar relay y verificar _readiness"]
    S6 --> S7["Verificar version NIP-11 y max version de _sqlx_migrations"]

El relay va último a propósito: arrancarlo antes de que la base y el bucket estén coherentes lo hace servir eventos que apuntan a blobs inexistentes, y el sondeo de conformidad de git contra S3 puede abortar el arranque (BUZZ_GIT_CONFORMANCE_PROBE está en true por defecto y su fallo es fatal, como vimos en Medios y almacenamiento).

just reset: qué destruye exactamente

just reset (Justfile:68-70) es una receta de desarrollo, no de producción, y tiene confirmación interactiva obligatoria vía el atributo [confirm(...)] de just:

This will DELETE all development data and preserve installed Buzz. Continue? (y/N)

Al confirmar ejecuta ./scripts/dev-reset.sh --yes, que hace en orden:

  1. scripts/reset-desktop-dev-state.sh — borra el estado de desarrollo de la app desktop.
  2. docker compose down -v --remove-orphans — para los contenedores y elimina todos los volúmenes del compose de desarrollo. Aquí es donde se pierden Postgres y MinIO locales.
  3. exec scripts/dev-setup.sh — recrea el entorno desde cero y vuelve a migrar.

La advertencia literal del script:

WARNING: This will DELETE all development data (desktop state, postgres, minio volumes).
   Installed Buzz app state and its production keyring are preserved.
   Redis data is ephemeral and always wiped on restart.

Dos precisiones: just reset opera sobre el docker-compose.yml de la raíz, el stack de desarrollo, y no toca deploy/compose/ — pero el -v es indiscriminado dentro de ese proyecto de compose. Y el equivalente en producción no existe: ./run.sh stop ejecuta compose down sin -v y conserva los volúmenes deliberadamente.

Actualizar de versión

Qué leer antes

Dos archivos, y no son intercambiables:

ArchivoDe qué habla
CHANGELOG.md (raíz)Cambios de la app desktop y compartidos. Encabezados ## v0.5.5, ## v0.5.4, …
crates/buzz-relay/CHANGELOG.mdCambios del relay. Encabezados ## relay-v0.2.0, ## relay-v0.1.1.
RELEASING.mdEl procedimiento de publicación completo, por carril.

Si estás operando un relay, el changelog que te importa es el del crate.

Versionado y canales de release

RELEASING.md describe tres carriles independientes, que versionan por separado:

CarrilEntradaArtefactoAutoridad de versión
Desktopjust release-desktop <version>App empaquetada firmadadesktop/package.json y manifiestos sincronizados
Relayjust release-relayImagen ghcr.io/block/buzzcrates/buzz-relay/Cargo.toml
Mobilescripts/mobile-release.sh candidate X.Y.ZTag inmutable mobile-vX.Y.Z-rc.NEl propio tag remoto

Para el relay el flujo es: just release-relay corre sobre main, abre o actualiza un PR relay-release/<version>, bumpea crates/buzz-relay/Cargo.toml, regenera Cargo.lock y actualiza el changelog del relay. Al mergear, el workflow auto-tag-on-release-pr-merge empuja el tag relay-v<version>, y ese tag dispara docker.yml.

Los tags de imagen que produce docker.yml están documentados en su cabecera:

DisparadorTags publicados
Push a main:main, :sha-<7>, más :debug-main y :debug-sha-<7>
Tag relay-v*.*.*:{version}, :{major}.{minor}, :{major} y sus :debug-*
Release estableAdemás mueve :latest y :debug-latest

Detalle que decide tu política de pinning: :latest sigue solo releases estables. metadata-action usa flavor.latest=auto, así que un relay-v0.3.0-rc.1 publica :0.3.0-rc.1 sin mover :latest, y los pushes a main nunca generan :latest. Los tags debug-* traen los mismos binarios optimizados pero con información de línea para profilers nativos; los normales van stripped y son el default de despliegue.

El README.md del bundle Compose es explícito sobre el default: BUZZ_IMAGE apunta a ghcr.io/block/buzz:main, pensado para pruebas tempranas, y para producción hay que pinnear ghcr.io/block/buzz:sha-<7> o un tag semver.

El chart Helm es un cuarto artefacto versionado aparte (Chart.yaml version: 0.1.7, appVersion: "0.1.0"), publicado como artefacto OCI en oci://ghcr.io/block/buzz/charts/buzz mediante tags chart-v*. Su image.tag vacío significa “usa .Chart.AppVersion”, que casi nunca es lo que quieres en producción: fíjalo explícitamente.

El orden seguro

sequenceDiagram
    participant Op as Operador
    participant BK as Backup
    participant DB as Postgres
    participant R as Relay
    participant C as Clientes
    Op->>BK: 1. Dump de Postgres, espejo de S3 y git, copia de secretos
    Op->>DB: 2. Migraciones aplicadas o auto-migrate habilitado
    Op->>R: 3. Pull de la imagen nueva y recreacion
    R-->>Op: 4. _readiness responde ready y NIP-11 muestra la version nueva
    Op->>C: 5. Recien ahora se actualizan las apps de escritorio

Con el bundle Compose el paso 3 es un solo comando, que además vuelve a imprimir la checklist de respaldo al terminar (run.sh:69-74):

cd deploy/compose

# 1. backup — ver seccion anterior
./run.sh backup-hint

# 2 y 3. actualizar BUZZ_IMAGE en .env y aplicar
$EDITOR .env               # BUZZ_IMAGE=ghcr.io/block/buzz:0.2.0
./run.sh config            # valida la config combinada antes de tocar nada
./run.sh upgrade           # compose pull + compose up -d --wait + backup_hint

# 4. verificar
./run.sh status
curl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' .env | cut -d= -f2-)/_liveness"
curl -fsS -H 'Accept: application/nostr+json' http://127.0.0.1:3000/ | jq .version

./run.sh upgrade es literalmente require_env + compose pull + compose up -d --wait

  • backup_hint. El --wait respeta el healthcheck del relay, que abre /dev/tcp/127.0.0.1/8080, hace GET /_readiness y busca 200 OK, con retries: 12 y start_period: 30s. Si la migración falla al arrancar, el contenedor no llega a sano y --wait devuelve error: eso es exactamente lo que quieres.

Si solo cambiaste variables de entorno o la imagen y no quieres tocar el resto del stack, ./run.sh restart hace compose up -d --wait --force-recreate relay.

En Kubernetes, con autoMigrate en true, helm upgrade es el procedimiento completo. Con autoMigrate en false, el buzz-admin migrate va antes, siempre.

helm upgrade buzz oci://ghcr.io/block/buzz/charts/buzz \
  --version 0.1.7 \
  -n buzz \
  --set image.tag=0.2.0 \
  --reuse-values \
  --wait

kubectl -n buzz rollout status deployment/buzz

Rollback

El rollback de binario es barato: vuelve a apuntar BUZZ_IMAGE al tag anterior y repite ./run.sh upgrade, o usa helm rollback en Kubernetes.

El rollback de esquema no existe. Las migraciones sqlx de Buzz son solo hacia adelante: no hay archivos down, y varias migraciones son destructivas por diseño (0007 es checksum-frozen y purga historial NIP-RS). Un relay viejo contra un esquema nuevo es territorio no soportado. Por eso:

  • El backup de Postgres anterior a la migración es tu único rollback real de datos.
  • Antes de un salto de varias versiones, prueba la migración sobre una copia restaurada del dump, no sobre producción.
  • Si la guarda pre-0007 aborta tu migración, no la esquives: repara las filas señaladas. Está ahí precisamente para evitar una pérdida irreversible.

Una nota tranquilizadora: la superficie que cambia en un upgrade es la que tú cambias en el .env. Por ejemplo BUZZ_MESH es false por defecto en el binario y solo on, true o 1 lo activan, así que un upgrade de imagen no abre puertos UDP nuevos solo.

Compatibilidad relay ↔ app de escritorio

Aquí conviene ser honesto sobre lo que el repositorio dice y lo que no dice.

Lo que sí dice:

  • Los carriles versionan de forma independiente. docker.yml lo comenta explícitamente: los tags v* de desktop y sprig-v* de agente no publican la imagen del relay; solo relay-v* lo hace, “so the relay image version tracks crates/buzz-relay/Cargo.toml, never desktop”. Un relay 0.2.0 y un desktop 0.5.5 son perfectamente normales: no son la misma escala de versión.
  • El contrato real entre cliente y relay es el protocolo, no el número de versión. El documento NIP-11 que sirve el relay en / (con Accept: application/nostr+json) y en /info publica supported_nips, supported_extensions (hoy incluye "nip-er"), limitation con auth_required y payment_required, y el campo self con la clave del relay cuando aplica. Los NIPs draft no se anuncian en supported_nips hasta tener número upstream: se anuncian como cadenas en supported_extensions.
  • La app de escritorio se actualiza por su propio canal: el release buzz-desktop-latest con su latest.json, que cubre las cuatro claves de plataforma (darwin-aarch64, darwin-x86_64, linux-x86_64, windows-x86_64). Si el updater reporta “no update available”, RELEASING.md manda verificar que ese release exista y contenga un latest.json válido; una entrada de plataforma ausente suele significar que ese job de release falló.

Lo que no dice: el repositorio no publica una matriz de compatibilidad relay-versión ↔ desktop-versión, ni un campo de versión mínima de cliente que el relay rechace. No inventes una. La consecuencia operativa práctica es la del orden seguro de la sección anterior: actualiza el relay primero y los clientes después. Un cliente viejo contra un relay nuevo pierde, como mucho, funciones que dependen de NIPs recién añadidos y que el cliente aún no sabe pedir; un cliente nuevo contra un relay viejo pide capacidades que ese relay todavía no anuncia en supported_nips.

Recuerda también que varias capacidades del listado son condicionales, no fijas: NIP-43 solo se añade a supported_nips cuando el relay tiene clave estable y aplica membresía, y el bloque push aparece únicamente cuando la entrega push está configurada. Un supported_nips más corto de lo esperado puede ser configuración, no versión: revísalo contra el capítulo Configuración antes de culpar al upgrade.

Por último, la tabla “Works today / Being wired up / Strong opinions” del README pone las notificaciones push y los clientes móviles fuera de la columna de lo que funciona hoy. Al planificar upgrades, trátalas como trabajo en curso, no como contrato estable.

Resumen

  • Buzz guarda estado en cuatro lugares independientes: Postgres, el bucket S3/MinIO, el volumen git y los secretos del .env. Redis es efímero y se descarta al reiniciar.
  • Las migraciones son sqlx embebidas en el binario vía sqlx::migrate!("../../migrations"); hoy son 28 archivos y su estado vive en la tabla _sqlx_migrations.
  • Se aplican por tres caminos: BUZZ_AUTO_MIGRATE al arrancar el relay, buzz-admin migrate por CLI, o just migrate en desarrollo. El default de BUZZ_AUTO_MIGRATE depende del contexto: opt-in en el binario y en compose.yml, true en deploy/compose/.env.example y en el chart.
  • La migración falla en cerrado: una guarda rechaza bases pre-0007 con filas de kind 30078 ambiguas, y otra verifica el trigger de piso de created_at de la migración 0021 en events y en cada partición.
  • Los readiness probes solo comprueban conectividad a la base, no frescura del esquema: un pod puede parecer sano contra un esquema sin migrar.
  • ./run.sh backup-hint imprime la checklist canónica de respaldo, y los snapshots de Postgres y de objeto/git deben venir de la misma ventana de mantenimiento.
  • Perder BUZZ_RELAY_PRIVATE_KEY significa un relay con identidad nueva; la clave del owner no vive en el stack y se restaura reinstalando con el mismo ownerPubkey.
  • just reset pide confirmación y ejecuta dev-reset.sh --yes, que hace docker compose down -v sobre el stack de desarrollo y borra todos sus volúmenes. No existe un equivalente para producción.
  • El relay se versiona con tags relay-v* desde crates/buzz-relay/Cargo.toml, y docker.yml publica :{version}, :{major}.{minor}, :{major} y :latest solo para estables; los pushes a main producen :main y :sha-<7>.
  • Orden seguro de actualización: backup → migraciones → relay → clientes. El rollback de imagen es trivial; el rollback de esquema no existe, así que el dump previo es tu única salida.
  • No hay matriz de compatibilidad publicada entre relay y desktop: el contrato es el documento NIP-11 con supported_nips y supported_extensions, y los carriles de release son independientes.

Siguiente: Seguridad en producción y runbook de operación