← SCRAM AI Lab
Patrón dual-write durante 30 días, backfill batch de 10K contactos por batch, redirect 301 al final. 47K contactos migrados en 6h sin interrumpir capturas.
May 21, 2026
10 lecturas
Llevamos cinco años viendo migraciones de Mautic terminar mal: cutover de viernes en la noche, lunes el equipo de marketing descubre que 8% de los contactos no llegaron, 15% de los segments se rompieron porque los filtros no mapean uno-a-uno, y el formulario de la landing principal estuvo apuntando al endpoint viejo durante 36 horas. El patrón que sí funciona tiene tres fases y dura un mes — pero nadie te despierta a las 3 AM.
Los primeros 15 días el CRM nuevo es la fuente de verdad para lectura, pero todo lo que se escribe (forms, tracker, sync ERP) se duplica a Mautic en background. Mautic queda en read-only para UI humana — nadie puede tocarlo manualmente. Esto compra ventana de rollback: si algo explota, apuntas las lecturas a Mautic en 5 minutos.
// services/contact-write.service.ts
async function captureContact(data: ContactInput) {
// Primario: CRM nuevo
const contact = await prisma.crmContact.create({ data });
// Espejo a Mautic (best-effort, no bloquea)
this.mauticMirrorQueue.add('mirror-contact', {
crmId: contact.id,
email: data.email,
phone: data.phone,
payload: data,
}, { attempts: 3, backoff: 'exponential' });
return contact;
}
En paralelo, un job nocturno baja contactos históricos de Mautic en batches de 10K. La parte crítica es la deduplicación compuesta: email exacto OR phone normalizado (sin lada, sin espacios). Mautic permite duplicados; tu CRM nuevo no debe.
async function backfillBatch(offset: number) {
const mauticContacts = await mautic.getContacts({
limit: 10000,
offset,
orderBy: 'date_modified',
});
for (const m of mauticContacts) {
const phoneNorm = normalizePhone(m.phone); // +52 5512345678
const existing = await prisma.crmContact.findFirst({
where: {
OR: [
{ email: m.email.toLowerCase() },
phoneNorm ? { phone: phoneNorm } : undefined,
].filter(Boolean),
},
});
if (existing) {
await prisma.crmContact.update({
where: { id: existing.id },
data: { mauticId: m.id, ...mergeFields(existing, m) },
});
} else {
await prisma.crmContact.create({ data: mapMauticToCrm(m) });
}
}
}
Números reales del caso AWALAB: 47K contactos, 12K segments, 6 horas de ejecución total. El cuello de botella no fue Postgres ni Mautic — fue la normalización de teléfonos con libphonenumber para detectar duplicados.
Después de 30 días de dual-write sin incidentes, dropeamos las escrituras a Mautic. Los endpoints viejos (/form/submit/mautic, /mtc.js) reciben redirect 301 a los equivalentes del CRM nuevo. Los 301 los procesa Traefik en el edge sin tocar Mautic — para entonces Mautic ya está apagado, solo Traefik conoce el mapeo.
# traefik dynamic config
http:
routers:
mautic-legacy:
rule: "Host(`mautic.scram2k.com`) || PathPrefix(`/mtc.js`)"
service: noop@internal
middlewares: [redirect-to-crm]
middlewares:
redirect-to-crm:
redirectRegex:
regex: "^https://mautic\\.scram2k\\.com/(.*)"
replacement: "https://api.scram2k.com/v1/tracker/$1"
permanent: true
Equipo que conozco intentó "lift and shift" en un fin de semana: export CSV de Mautic, import al CRM nuevo, apagar Mautic. Resultado: 12% de contactos con teléfono malformado quedaron como duplicados, segments dinámicos se convirtieron en listas estáticas (perdiendo la lógica), tracking JS apuntaba a endpoint inexistente por 4 días hasta que alguien notó que el formulario de demo no estaba capturando leads. Cutover hard sin shadow period es la receta para el postmortem.
La pregunta de seguimiento: ¿cuánto del código de espejo a Mautic se reusa para futuras migraciones (HubSpot, Salesforce)? Spoiler: el 80% si abstraes el adapter desde el día uno.
Artículos relacionados
RPA REPSE/IMSS con Playwright + undetected-chromedriver
Los portales SAT/IMSS detectan headless browsers. Combinación ganadora: undetected-chromedriver para Python + Playwright para flow control. 2captcha y nunca almacenar credenciales del cliente.
INEGI BIE/DENUE para context grounding
Inyectar datos reales de INEGI (16M series económicas, 5M empresas geolocalizadas) como context grounding mejora respuestas de LLM sobre mercado mexicano. MCP tool definition y patrón de detection.
Bind ERP como MCP server: el caso SCRAM
Exponer Bind ERP via Model Context Protocol en vez de un wrapper API por cliente. Una capa, varios LLMs (Claude, Cursor, GPT) hablando con el mismo ERP. Rate limits y Redis incluido.