Saltar al contenido principal
El store es donde el bot guarda mensajes, chats, contactos, checkpoints de flujos, historial de conversaciones de IA y los datos de ctx.user. Con el store en memoria por defecto, un reinicio lo olvida todo; con uno persistente, el bot sigue donde lo dejó. Todos los stores implementan el contrato IStore de @botmesh/core, neutral respecto al transporte, así que el mismo store funciona con cualquier canal.

Elegir un store

Reglas prácticas:
  • Un proceso del bot en un VPS o en un contenedor: SQLiteStore. Sin servidor que mantener, un archivo que respaldar.
  • Varios procesos del mismo bot (réplicas), o estado que leen otros servicios: RedisStore. Todas las réplicas deben ver los mismos datos, así que MemoryStore y SQLiteStore no sirven ahí. Si ya tienes PostgreSQL, MySQL/MariaDB o MongoDB, el store correspondiente hace lo mismo.
  • Pasa siempre una instancia de store (store: new SQLiteStore({ … })). Un string como store: 'sqlite' solo se acepta por compatibilidad y sigue creando un MemoryStore.
El bot llama por ti a start() y stop() del store.

MemoryStore

Sin la opción store, el bot crea un MemoryStore. Todo vive en RAM. Un archivo JSON opcional guarda una copia de chats, contactos, checkpoints de flujos, conversaciones de IA y datos de ctx.user (no los mensajes) cada cierto intervalo y en stop():
El log de mensajes tiene un tope de 1000 mensajes por chat por defecto porque vive en RAM. Los stores persistentes no tienen tope salvo que pongas uno.
Los mensajes no se restauranEl snapshot JSON omite a propósito el log de mensajes (tamaño y privacidad). Usa SQLiteStore o RedisStore cuando el historial deba sobrevivir a un reinicio.

SQLiteStore

Una base de datos real en un archivo, sobre better-sqlite3. better-sqlite3 es una peer dependency, así que instálala junto al store:
La carpeta de file debe existir: SQLite crea el archivo, no sus carpetas.

Instalar better-sqlite3

better-sqlite3 es un módulo nativo. El rango soportado es ^11.7.0 || ^12.0.0 || ^13.0.0, y el store se prueba con los tres.
Node.js 24.19 o superior: usa better-sqlite3 13Las versiones 11 y 12 pueden abortar el proceso (Assertion failed: (env) != nullptr en RemoveEnvironmentCleanupHook) cuando el recolector de basura libera un prepared statement, por una regresión de Node.js (nodejs/node#65446). La versión 13 usa N-API y no está afectada.

Opciones

Un número inválido (negativo, decimal) lanza un RangeError en el constructor.

Qué almacena

Versiones del esquema y actualizaciones

El archivo guarda su estructura en PRAGMA user_version; SQLITE_SCHEMA_VERSION es la versión que escribe esta release. Al abrir un archivo se ejecutan en orden las migraciones pendientes, cada una en su propia transacción, así que una base escrita por una release anterior se actualiza en el sitio conservando sus filas. Nunca creas ni migras tablas a mano. Un archivo escrito por una release más nueva se rechaza, sin tocarlo, con SQLiteSchemaVersionError (code: 'SQLITE_SCHEMA_TOO_NEW', con found y supported). No se admiten bajadas de versión, así que:
  • Respalda el archivo antes de actualizar @botmesh/sqlite-store si puedes necesitar volver atrás.
  • Si varios procesos comparten un archivo, para los de la versión antigua antes de que uno nuevo lo abra.
El README del paquete detalla qué cambió en cada versión y cómo deshacer el último paso sin copia de seguridad.

Ciclo de vida

bot.stop() (y un bot.start() fallido) cierra la base de datos, y el siguiente bot.start() la reabre, así que una misma instancia de SQLiteStore, con los backends schedules() y durability() que entregó, sobrevive a ciclos de parada y arranque. SQLite guarda el texto en UTF-8, donde un surrogate UTF-16 suelto se convierte en U+FFFD y dos ids distintos podrían compartir fila. saveMessage y setCursor rechazan ese id, clave o valor de cursor con un TypeError.

Flujos persistentes

Con un store persistente, los checkpoints de flow(...).persistent() sobreviven a un reinicio:
Si el proceso se detiene a mitad del flujo, el siguiente mensaje del usuario lo retoma en el paso donde se quedó. Ese mensaje solo despierta el flujo: la pregunta pendiente se envía otra vez (¿Y tu email? arriba) y la siguiente respuesta del usuario es la que cuenta. Con durabilidad, el mensaje que lo despierta se toma como la respuesta. Se escribe un checkpoint al empezar el flujo y después de cada paso, así que incluso un flujo que aún espera su primera pregunta se retoma ahí. Un reinicio a mitad de un .form() vuelve a preguntar el formulario desde su primer campo: todo el formulario es un solo paso.

RedisStore

Estado compartido por todos los procesos de un bot, sobre Redis (node-redis 4, 5 o 6 como peer dependency):
El mismo RedisStore entrega los demás backends de Redis, todos sobre su cliente y su prefijo: store.coordinator() para réplicas, store.schedules() y store.leases() para el scheduler durable. El backend de durabilidad es new RedisDurability({ client, prefix }) (ver Procesamiento durable). Un cliente inyectado sin listener de 'error' tumba el proceso en la primera caída de conexión (node-redis relanza el evento), así que ponle siempre uno. El esquema de claves y el resto de detalles están en el README del paquete.

PostgreSQL, MySQL y MongoDB

@botmesh/postgres-store, @botmesh/mysql-store y @botmesh/mongo-store guardan el estado del bot en la base de datos que ya operas, compartida por cualquier número de réplicas. Pasan la misma suite de conformidad que los demás stores, crean sus tablas (o índices) en start() y traen un store de schedules durable (store.schedules()). PostgreSQL tiene además un backend de durabilidad, PostgresDurability. El driver de la base de datos es una dependencia peer y solo se carga cuando el store crea su propio pool o cliente:
Todos aceptan también un pool o cliente existente (pool / client), que nunca cierran. Con un pool de pg inyectado, añade un listener pool.on('error', …): node-postgres tumba el proceso cuando se cae una conexión ociosa y nadie escucha. En PostgreSQL, tablePrefix admite como mucho 42 caracteres (el servidor trunca los nombres a 63 bytes; uno más largo lanza RangeError). Las opciones, el esquema y las tablas de errores están en el README de cada paquete: PostgreSQL, MySQL/MariaDB, MongoDB.

Tablas de datos de usuario

Los perfiles de ctx.user se guardan junto al resto del estado del bot, respetando tablePrefix / collectionPrefix: Las entradas caducadas se borran de forma perezosa con bot.purgeExpiredUserData() (Redis las caduca solo). Los stores SQL y Mongo las borran en lotes de purgeBatchSize (por defecto 1000) hasta que no queda nada caducado, así que un gran atraso nunca se ejecuta como un único borrado largo.

Escribir tu propio store

Implementa IStore para conectar otro backend. Quien escribe un store importa el contrato y sus helpers desde @botmesh/core/spi:
La interfaz completa está en la referencia de la API. MemoryStore es la implementación de referencia, y dentro del monorepo todos los stores pasan la suite de conformidad compartida (test-utils/store-conformance.ts). Semántica que todo store debe respetar:
  • upsertChat / upsertContact hacen merge superficial sobre el registro guardado. Un campo propio con valor undefined borra ese campo, así que un upsert parcial debe omitir lo que no conoce. El bot ya lo hace: un mensaje sin nombre visible nunca borra el guardado.
  • deleteChat borra también el log de mensajes de ese chat.
  • getMessagesByChat(chatId, limit) devuelve los limit mensajes más recientes, del más antiguo al más nuevo (por defecto 50). Un limit de 0 o menos no devuelve nada, una fracción se redondea hacia arriba e Infinity devuelve todo el log. Los stores persistentes ordenan por timestamp, así que volver a guardar un mensaje con otro timestamp lo mueve; MemoryStore lo deja en la posición de su primer guardado.
  • saveMessage ignora un mensaje sin ref.id o sin chatId (los providers mandan ref.id: '' cuando no tienen un id estable), y getMessage de esa ref resuelve undefined. Volver a guardar un id reemplaza el mensaje.
  • maxMessagesPerChat es opcional en los stores persistentes. Rechaza un tope negativo o decimal con un RangeError.
  • setUserData rechaza con un TypeError un valor que no es JSON (undefined) y no escribe nada.
  • Las filas de mensajes se indexan con messageKey(ref) (de @botmesh/core/spi): {chatId}:{id} con % y : escapados en cada parte (%25, %3A), para que los ids con : (los chat ids whatsapp:+1… de Twilio) nunca choquen. Las filas escritas con la clave antigua sin escapar (legacyMessageKey(ref)) deben seguir leyéndose y borrándose, comprobando que el ref.id y el ref.chatId guardados son los del mensaje pedido, y al volver a guardarlas pasan a la clave nueva.
Métodos opcionales:
  • Datos de usuario (getUserData, setUserData, deleteUserData, listUserData, purgeExpiredUserData): sin ellos, ctx.user rechaza con UNSUPPORTED_STORE. Compruébalos con test-utils/user-data-conformance.ts.
  • Cursores (getCursor/setCursor): además persisten las marcas de WELCOME y el índice de handoff.
  • Checkpoints condicionales (setCheckpointIf/clearCheckpointIf): necesarios para un relevo seguro entre réplicas. Sin ellos, el bot avisa en el log cuando la coordinación está activa.

Docker

Monta la carpeta de datos como volumen para que la base de datos sobreviva a los reinicios del contenedor. Ver Despliegue para la configuración completa y las copias de seguridad.