Operación y pruebas
Backups, migraciones, pruebas, E2E y solución de problemas.
En esta página
Copias de seguridad#
BloomX no incluye herramientas de backup: dependen de tu proveedor. Qué proteger:
| Qué | Dónde vive | Recomendación |
|---|---|---|
| Base de datos del frontend | PostgreSQL (DATABASE_URL) | Backups automáticos y recuperación a un punto en el tiempo del proveedor (Neon, RDS…); prueba la restauración |
| Base de datos del backend | PostgreSQL del backend | Ídem. Contiene dominios, claves públicas de firma, extensiones instaladas y credenciales cifradas |
| Correos y adjuntos | Bucket S3/B2/R2 | Versionado del bucket y replicación; activa S3_SSE |
| Claves | NEXTAUTH_SECRET, DATA_ENCRYPTION_KEY (+ DATA_ENCRYPTION_KEYS_PREVIOUS), MFA_RECOVERY_PEPPER, BLOOMX_DOMAIN_PRIVATE_KEY | Sin estas claves los datos cifrados y las sesiones no se recuperan. Guárdalas en un gestor de secretos, separadas del backup de la base de datos |
Tras restaurar una base de datos, ejecuta npm run db:ensure (idempotente) y comprueba GET /api/config. Si restauras a un entorno nuevo, TOP_DOMAIN y el dominio registrado en el backend deben coincidir.
Migraciones#
Frontend: `db:ensure`#
npm run db:ensure(scripts/ensure-schema.ts) crea o completa el esquema conCREATE TABLE IF NOT EXISTS,ADD COLUMN IF NOT EXISTS, constraints con guarda e índicesIF NOT EXISTS, más de 25 tablas (usuarios, correos, adjuntos, etiquetas, reglas, MFA, revocación, auditoría, calendario, citas, contactos, Elixir, mensajes sellados, push…). Corre solo enpredev,prebuildyprestart.- Es aditivo, no un sistema de migraciones versionadas: no cambia el tipo de una columna existente ni la renombra. Es idempotente y seguro de ejecutar varias veces; usa un *advisory lock* de transacción por tabla, así que varias instancias arrancando a la vez no chocan.
- Excepción de limpieza: elimina la columna heredada
Email.accountEmaily su índice. Los datos de negocio no se modifican. - Índices notables: GIN de texto completo sobre
Email, índice único parcial de idempotencia de envío, índice único parcial anti doble reserva de citas (se omite con un aviso si ya hay duplicados) y(campaignId, recipient)único en Elixir. - Sin
DATABASE_URLsolo avisa y sale con 0; si falla, sale con 1 (y rompe el build).
Backend#
- No hay
ensure.npm run prisma:pushaplicaprisma/schema_push.prisma(igual queschema.prismamásDomain.signingPublicKey). No hay carpetaprisma/migrations. prisma db pushpuede proponer cambios destructivos: revisa el plan y no uses--accept-data-lossa ciegas.- La columna
signingPublicKeytambién se crea de forma perezosa (ADD COLUMN IF NOT EXISTS) al registrar la primera clave.
Pruebas#
# Frontend
npm test # vitest: sin red, sin servicios reales (DATABASE_URL apunta a un destino inalcanzable)
npm run test:pg # vitest contra un PostgreSQL embebido y efímero (*.pg.test.ts); nunca toca tu BD
npm run test:db:setup # crea prisma/test.db (SQLite) que usan algunos tests
npm run check:themes # contraste WCAG AA de temas genéricos y de empresa; sale con 1 si algo falla
npx tsc --noEmit # tipos
# Backend compartido
npm test # node --test (firmas, dominio legado/firmado, CORS, rutas con dobles, sandbox)
# Extensiones (Node >= 22.6)
cd bloomx-extensions && npm test- Frontend: unos 105 archivos de test y más de mil casos (
npm test,vitest), de los cuales ~120 corren contra PostgreSQL real embebido connpm run test:pg. Elsetupde vitest bloquea la base real: los tests nunca tocan Neon ni tuDATABASE_URL. - Backend: 12 archivos, ~130 casos con el runner de Node. Extensiones: contrato, DLP, grupos, organizer, webhooks y slash-commands.
- Este
/docstiene su propio test (src/app/docs/_content/__tests__/docs.test.ts): comprueba que los enlaces internos y las anclas de la navegación existan, que cada página exista en español e inglés con la misma estructura y que toda variable de entorno de las tablas exista en un.env.exampleo en el código. npm run check:themesvalida el contraste AA de los temas (Temas).
E2E local#
Un entorno E2E manual y aislado (scripts/e2e-README.md): Postgres embebido, Resend y backend falsos y next dev con un .env.e2e generado. No hay un script npm que lo lance: es un procedimiento manual.
# sin Docker, todo en 127.0.0.1 con datos de prueba (nunca usa tu .env real)
node scripts/e2e-pg.mjs # PostgreSQL embebido (puerto 54329, base bloomx_e2e)
npx tsx scripts/seed-e2e.ts # genera .env.e2e, aplica el esquema y siembra usuarios/correos
node scripts/fake-resend.mjs # Resend falso (54330): guarda los envíos en .e2e/resend-captured.json
node scripts/fake-backend.mjs # backend falso (54331) con el manifest real de slash-commands
node scripts/e2e-dev.mjs # next dev SOLO con .env.e2e en el puerto 3100 (NO uses npm run dev)
node scripts/e2e-webhook.mjs # webhook email.received firmado con Svix (BAD_SIG=1 para probar firma inválida)- Usuarios de prueba:
tester@bloomx.testy el administradoradmin@bloomx.test(con MFA obligatorio); las contraseñas están en el.env.e2egenerado. Scripts auxiliares:e2e-totp.ts(códigos TOTP),e2e-session-replay.mjsye2e-tracker.mjs. - Para limpiar:
rm -rf .e2e .gemini/storage .env.e2e prisma/.pgdata. - No se probó contra servicios reales: Neon con pooler, S3/B2, Resend ni Google reales.
Solución de problemas#
| Síntoma | Causa probable y solución |
|---|---|
Cualquier /api/* responde 307 a /login | No hay cookie de sesión válida (o el JWT no es de sesión). Inicia sesión y reenvía la cookie; para clientes externos usa la API molt. |
| El login de un administrador responde 503 | Falta la tabla UserMfa y MFA es obligatorio para ese correo. Ejecuta npm run db:ensure. |
/api/register responde 403 Invalid registration secret | La clave no coincide, o en producción REGISTRATION_KEY falta o vale dev-secret. |
GET /api/config devuelve "BloomX Default" | El dominio no está registrado en el backend, o TOP_DOMAIN/Host no coincide con el dominio registrado. En el backend TOP_DOMAIN prevalece sobre ?domain. |
El backend responde 401 Unauthorized a un dominio con clave | Firma ausente/inválida, reloj desfasado más de 120 s, nonce repetido o X-BloomX-Domain ausente. Sincroniza el reloj (NTP) y revisa que firmas los mismos bytes del cuerpo (protocolo). |
Las cabeceras del backend indican X-BloomX-Auth: legacy | El dominio no tiene clave registrada o el frontend no tiene BLOOMX_DOMAIN_PRIVATE_KEY. Los hooks EMAIL_RECEIVED/CRON y services.mail no funcionan en modo legado; las credenciales de extensiones sí. |
El Organizer falla con ORGANIZER_MAIL_SERVICE_UNAVAILABLE | Falta el modo firmado en tu dominio (BLOOMX_DOMAIN_PRIVATE_KEY; ya no se necesita ninguna clave en el backend) (o la extensión no declara READ_EMAIL/MAIL_LABEL). |
El webhook de Resend responde 400 Invalid signature | Copiaste en WEBHOOK_SECRET el secreto de otro webhook (cada uno tiene el suyo), o el proxy altera el cuerpo. Sin la variable, el webhook se acepta sin firma. |
| Llegan correos pero sin adjuntos | Revisa el almacenamiento (S3_*) y los logs de process-attachments; npm run attachments:reprocess o POST /api/admin/reprocess-attachments los reprocesan. |
Un envío responde 422 EXTENSION_BLOCKED | Un hook EMAIL_PRE_SEND (por ejemplo DLP) lo bloqueó; el mensaje explica la política. |
| Un envío responde 425 | Hay otro envío con la misma Idempotency-Key en curso: reintenta tras Retry-After: 2. |
| Las campañas de Elixir no avanzan | No hay cron sobre /api/cron/elixir ni la interfaz abierta; define CRON_SECRET y un cron/pinger (Despliegue). Sin tablas: {processed:0, note:"elixir_tables_missing"} → db:ensure. |
| El rate limit parece inconsistente | Sin Upstash es por instancia. Configura UPSTASH_REDIS_REST_URL y UPSTASH_REDIS_REST_TOKEN. |
| Las sesiones se cierran tras un despliegue | Cambiaste NEXTAUTH_SECRET, el dominio o SESSION_COOKIE_HOST_PREFIX. Fija MFA_RECOVERY_PEPPER antes de rotar. |
| La subida devuelve 422 o 503 | 422: el antivirus detectó una amenaza. 503: no se pudo escanear con AV_FAIL_MODE=closed. |
next build falla con DATABASE_URL | prebuild ejecuta db:ensure; define la variable en el entorno de build o accede a una base alcanzable. |
| Los estilos de empresa no aparecen | Comprueba GET /api/config (theme), npm run check:themes y que borraste la cookie bloomx-theme/localStorage para ver defaultMode. |
Cómo se actualiza la PWA#
- BUILD_ID:
next.config.jslo genera en cada build (VERCEL_GIT_COMMIT_SHAsi existe; si no, un hash corto de fecha y azar fijo durante ese build) y lo expone comoNEXT_PUBLIC_BUILD_IDyNEXT_PUBLIC_BUILT_AT. - `GET /api/version`: público, sin sesión y con
Cache-Control: no-store. Devuelve{ buildId, clientApi, minClientApi, builtAt };clientApiesCLIENT_API_VERSION(la del build),announcedClientApies lo que la instancia anuncia al backend de extensiones (menor sinBLOOMX_DOMAIN_PRIVATE_KEY),signedindica si tiene clave yminClientApisale deBLOOMX_MIN_CLIENT_API(entero, por defecto 1). - Service worker: se registra como
/sw.js?v=<buildId>(sin caché HTTP,updateViaCache: none). Las cachés se nombran con el build id yactivateborra las de builds anteriores; no hay versión manual que subir. - Aviso: el cliente consulta
/api/versional cargar, al volver a la pestaña, al recuperar la red y cada ~10 min (±20%). Si el build cambió muestra «Hay una versión nueva · Actualizar»; al pulsar activa el SW nuevo (SKIP_WAITING) y recarga. Nunca recarga solo si hay un borrador con contenido o peticiones pendientes en la cola offline: espera a que el usuario termine. - Actualización obligatoria: si el
clientApidel cliente es menor queminClientApi, o una respuesta del backend traeX-BloomX-Min-Client-Apimayor queCLIENT_API_VERSION, aparece un aviso bloqueante «Actualizar ahora» y se recarga automáticamente (los borradores se guardan al descargar la página y la cola offline persiste). - Sin red no ocurre nada. Para forzar a todos a actualizarse, sube
BLOOMX_MIN_CLIENT_APIy despliega.
Qué vigilar#
- Logs de
AuditEvent(login, MFA, admin, assets) y avisos como[BLOOMX_AUTH] … LEGADO. - Fallos del webhook de Resend (500) y campañas Elixir en estado
failed/filaserror. - Presupuestos de extensiones (408,
Sandbox busy). - Retención: ejecuta
?dryRun=1antes de activar purgas nuevas.