DSF Research · Memorando Técnico VOL. III · NO. 7 20 MAY 2026

Viabilidad · Migración de Backend · Arquitectura

Sobre mover
DSF a
Podio.

Una auditoría a fondo de qué sobreviviría el viaje, qué se rompería en el camino, y qué ni siquiera podría abordar.

La promesa de la app móvil es conversación en tiempo real con un abogado, push que despierta el dispositivo, OTP que llega por WhatsApp, y un pipeline de medios que en silencio recodifica un video 4K en algo que una conexión 4G pueda cargar. Medimos cada uno contra la API de Podio tal como existe hoy en mayo 2026 — rate limits, techos de archivos, firmas CometD, topes de Items, y todo lo demás.

Si querés una sola oración: las partes que hacen que esta app móvil se sienta viva — chat, push, OTP, medios — no pueden correr en Podio, y las partes que podrían correr ahí llegarían lisiadas por los rate limits per-user.

Mover las funciones pesadas de DSF a Podio no es viable. Podio es una plataforma de work-management cuya envoltura técnica — 1 000 llamadas por usuario por hora, 100 MB por archivo, sin chunked uploads, sin real-time desde el bearer móvil, tope de 100 fields por app, sin compute server-side — fue diseñada para flujos CRM staff-side con unos pocos miles de records.

Lo que puede moverse: perfil, metadatos low-volume de tareas y casos, status workflows simples, almacenamiento de documentos. Lo que tiene que quedarse: chat, push, OTP, procesamiento de medios, almacenamiento particionado, búsqueda FTS, y los queue workers detrás de los ciclos MEP y Amparo. El desenlace honesto es un híbrido: DSF3 sigue siendo el system of record; Podio se vuelve el workspace agent-side, espejado vía webhooks.

Routes auditadas

644api

sobre la superficie dsf3/backend Laravel, mayo 2026

Tope de Podio

1k/hr/user

250/hr en endpoints rate-limited; per user, per key

Features móviles

15blockers

Capacidades que Podio no puede entregar del todo

Costo realista de seats

$28k/mes

2 000 leads activos × Plus tier, si fueran usuarios de Podio

§ 01El perfil de carga hoy

Antes de medir Podio tenés que medir lo que tendría que absorber. Un Explore agent recorrió el repo hermano dsf3/backend y contó todo. La forma que emerge no es un CRM low-traffic. Es un sistema consumer-facing real-time de volumen medio, con tabla de chat particionada, observers de denormalización, una queue de push dedicada, ffmpeg, Whisper, OpenAI Vision, y quince parsers especializados de PDF que alimentan un motor de workflows.

Fig. 1 La superficie dsf3/backend a mayo 2026. Cada barra es un file count o route count en el repo de producción. El cluster naranja de arriba — tablas de chat particionadas, comandos mensuales de mantenimiento de particiones, denormalización por observers — es la parte que más se subestima cuando alguien dice "movamos el chat nomás".

Algunos de estos conteos merecen una oración. Los 153 models incluyen nueve hermanos ChatMessage*; chat_messages en sí está particionado BY RANGE (TO_DAYS(created_at)) con particiones mensuales pre-creadas doce meses por adelantado. Los 42 jobs incluyen Chat\BroadcastMessageJob en una queue realtime, Chat\GenerateThumbnailJob que corre ffmpeg y calcula un blurhash, Chat\ProcessVideoJob que transcodifica uploads web a H.264 720p, Chat\TranscribeAudioJob en una queue transcription aparte, y Push\SendPushTokenJob en una queue push dedicada con cuatro supervisor workers.

Los 52 console commands incluyen mantenimiento de particiones (chat:create-monthly-partitions, chat:drop-old-partitions, chat:backfill-search-shadow), snapshots diarios de KPI, auto-advance de MEP, poda de particiones de telemetry, y un sampler de un 1% que corre EXPLAIN PARTITIONS y emite un evento de telemetry para observabilidad en producción. Ninguno tiene equivalente en Podio.

Las 327 migrations contienen DDL no trivial — composites UNIQUE de Idempotency-Key, tablas shadow de FULLTEXT, reescrituras polimórficas de read-receipts, reorganizaciones de particiones. Los 150+ services incluyen 80+ calculadores de KPI, 15 parsers de PDF, ImageValidation con OpenAI Vision, transcripción Whisper, PDF con Gotenberg, FCM/WebPush, sync con ERP. Los 30 broadcast events incluyen el cuarteto de chat (message.sent / .updated / .deleted / .read), whispers client-typing, task.updated, case.updated, y nueve variantes de eventos de rewards.

La app móvil, por su parte, llama unos 40 paths HTTP únicos y se suscribe a siete eventos de Pusher. Cada uno tiene un contrato que Podio tendría que reemplazar, debilitar, o quitarse del medio.

§ 02La envoltura real de Podio

Podio en 2026 es producto de Progress Software, no de Citrix. Sobrevivió la adquisición pero sus docs de developers no han crecido de manera significativa desde 2015. El feature set que anuncian es generoso; los límites debajo no.

Propiedad y estado estratégico

Citrix le vendió Podio (junto con ShareFile) a Progress Software el 31 de octubre de 2024. El portal de developers sigue en developers.podio.com; el help user-facing ahora vive en docs.sharefile.com/.../podio/. GlobiFlow, el add-on premium de workflows de toda la vida — luego rebautizado "Citrix Podio Workflow Automation" — está reportado como discontinuado; Progress renombró la superficie como Workflow Automation. No hay EOL anunciado — pero el público developer es chico, el roadmap público es opaco, y Progress no es una empresa SaaS de colaboración. Tratá el rumbo de Podio bajo el nuevo dueño como un known unknown.

La política de rate limit

Esta es la política que decide en silencio todas las demás preguntas. De developers.podio.com/index/limits:

Textual "The general limit is 1 000 API calls per hour. If the API call is marked as 'Rate limited' in the API reference the call is deemed resource intensive and a lower rate of 250 calls per hour is enforced. Rate limits are per user per API key. If you hit the rate limits the API will begin returning 420 HTTP error codes for all API calls."

La respuesta lleva dos headers — X-Rate-Limit-Limit y X-Rate-Limit-Remaining — pero, en palabras del propio Podio: "you cannot currently see when your rate limit counter is reset." Los aumentos requieren un ticket de soporte por cada client ID, abierto con el equipo de soporte de ShareFile de Progress. No hay header retry-after en el 420.

Items, la primitiva de records

Cada app — el nombre que Podio le da a un schema de records — tiene techo de 500 000 items, 100 fields, 5 000 revisiones rastreadas, y 20 000 filas en import/export de Excel. Los fields de app-reference topan las relaciones en 250 items. Los comments topan en 2 500 por item con un techo de 1 000 caracteres por mensaje. Los archivos topan en 200 por item. El campo de chat interno del propio Podio tiene, según sus docs, "1 000 characters max" — pista silenciosa de que nadie en Podio jamás pensó este producto para hospedar mensajería long-form.

Files

Tope duro de 100 MB por archivo. Sin chunked uploads ni resumables documentados — hacés POST del archivo entero en un round-trip HTTP. Para adjuntar a un item primero subís, luego llamás un endpoint attach-file aparte: dos golpes al rate limit por attachment. Los archivos tipo .exe, .bat, .com, .hta, o application/octet-stream pelado se rechazan — relevante porque RN Android a veces despoja el Content-Type a octet-stream en bodies multipart (memoria: feedback_rn_android_strips_content_encoding.md).

Push — CometD sobre Bayeux

El "push service" en https://push.podio.com/faye es long-polling con CometD/Bayeux. Para suscribirte a un canal hacés fetch de un private_pub_signature + private_pub_timestamp per-canal vía una llamada API regular (que descuenta de tu rate budget), y después lo metés en el campo ext del handshake de CometD. No hay SDK móvil documentado; el único código publicado son ejemplos jQuery + Faye para browser. Reconectar en cada foreground de la app cuesta llamadas API. CometD es materialmente más pesado en un teléfono que el envelope WebSocket de Pusher que usás hoy.

Webhooks

Cinco endpoints: create, delete, list, request-verification, validate-verification. Eventos: item.create/.update/.delete, comment.create/.delete, file.change, app.update/.delete, más eventos space-level y variantes field-scoped. El contrato tiene tres filos cortantes:

  • HTTPS solo en ports 80 / 443 — nada de custom ports.
  • Deadline de respuesta de 15 segundos antes de que Podio considere fallida la llamada.
  • 50 fallos consecutivos auto-desverifican el hook. Modo de falla silencioso a menos que vos te hagás el health-check.
  • El payload signing no está documentado. Te protegés con un shared secret en la URL.

Conversations

Existen 21 endpoints — create, reply, search, mark-read, star — pero los docs no publican máximo de participantes, longitud máxima de mensaje, reglas de attachments, ni semántica de edit/delete. Las Conversations son entre usuarios de Podio. Tus leads no son usuarios de Podio; son filas Person. Convertirlos a todos en usuarios pagos de Podio es la única forma de usar la primitiva Conversations nativamente — y ese es el movimiento cuyo precio se examina abajo.

Pricing (2026)

Free tope en 100 items por organización en total — inservible más allá de un demo. Plus son $14/user/mes ($11.20 anual). Premium — el que incluye Workflow Automation — son $24/user/mes ($19.20 anual). "Enterprise" se cotiza a la medida. Per-user quiere decir per cuenta humana de Podio.

§ 03Viabilidad feature por feature

Las features móviles bajo discusión, mapeadas contra la superficie real de capacidades de Podio. Las bandas de color no son un juicio subjetivo — son consecuencia directa de los límites en §02.

Feature móvil Qué hace hoy (DSF3) Primitiva más cercana en Podio Viabilidad
Chat lead⇄agente chat_messages particionado, envelope tight de Pusher, Idempotency-Key, chunked uploads, FTS5 + shadow FULLTEXT, tombstones de edit/delete, blurhash, whispers de typing API Conversations, comments sobre items, API Files No Rate limit + sin chunks + sin push móvil
Búsqueda dentro del chat FTS5 local + LIKE recent/year + shadow FULLTEXT scope=all API Search, clase rate-limited No Techo de 250 calls/hr
Indicador de typing Whisper de Pusher client-typing typing de CometD en canal de conversación No Auth per-canal quema budget
Read receipts chat_read_receipts polimórfico, PATCH batched por viewport Mark-as-read de conversación Parcial Sin granularidad per-mensaje
Tareas Task linked a opp, status workflow, real-time task.updated Podio Tasks nativos o Item-per-task Parcial Sin real-time móvil
Encuestas (wizard CCSS/MEP) Wizard sin scroll schema-driven, show_if condicional Item-per-respuesta; schema en código Parcial El wizard se queda client-side; la lógica de backend no se mueve
Casos Opportunity → vertical → progresión de step, real-time case.updated Item + status workflow + Workflow Automation Parcial Auto-advance OK; el pipeline MEP/AI no se mueve
TSE lookup Fetch directo del cliente a dsf.cr/tse/{cedula} N/A Ya es client-side
Perfil SecureStore + mirror MMKV de la fila Person Item-per-Person o API Contacts A través de un proxy DSF3
Terceros (árbol de reps) representative_id FK, descendientes recursivos Field de app-reference, tope de 250 items Parcial Tope de 250 / sin primitiva de recursión
OTP por WhatsApp Evolution API + cache registration:{cedula} + cooldown No Podio no manda SMS / WhatsApp / email-OTP
Push al móvil SendPushTokenJob per-token en queue push, FCM v1, web push VAPID, supresión inteligente cuando está online No El push de Podio es solo para sus propios clientes
Voice agent (LiveKit) Mint de room token, driver ElevenAgents No Las signing keys no pueden vivir en Podio
Ingesta de Telemetry device_logs particionado, EventRedactor, sampler No Un solo budget de 1k/hr se quema en un batch
Ciclo de reclamo MEP 15 parsers de PDF + OpenAI Vision + Gotenberg + cron de MEP auto-advance + 9 jobs Mep* Workflow Automation → webhook externo No El compute pesado no corre en Podio Flows
Procesamiento de medios Manipulator del cliente + ffmpeg en GenerateThumbnailJob + ProcessVideoJob + blurhash + Whisper No Sin compute server-side
KPI dashboards 80+ services que agregan data particionada en snapshots diarios Items + Workflow Automation No La API Items no puede GROUP BY day a 6 meses
Rewards / gamificación 40 reward models, badge engine, leaderboards, streak service Item-per-badge + triggers de Flows Parcial La superficie calza; el motor de criterios se queda
Link previews oEmbed + OG scraping, cache 24 h No Fetch server-side que no podés delegar
Preguntas de seguridad security_verified per-device, iconos por slug Items + Items Parcial Posible pero high-friction por ningún valor
Fig. 2 Distribución de las veinte features móviles por viabilidad en Podio. Los doce "No" contienen todo aquello sobre lo que descansa la propuesta de valor de la app; los seis "Parcial" son la zona de negociación; el único "Sí" es perfil; el resto ya está fuera de scope.

§ 04Los quince deal-breakers

Estas son las cosas que la app móvil necesita y Podio no entrega. Cada una, por sí sola, alcanza para descalificar una migración total. Listadas más o menos en orden de prioridad.

  1. Chat en tiempo real a escala consumer

    El envelope tight de Pusher de hoy son 400 bytes por mensaje, respaldado con una ventana de dedup de una hora vía Idempotency-Key sobre un UNIQUE composite. El rate limit de 1 000-calls-per-user-per-hour de Podio, la falta de un SDK de push móvil, y el modelo de conversation-as-Podio-user rompen cada uno de estos requerimientos por separado.

  2. Push notifications hacia la app móvil

    El push service de Podio es para sus propios clientes. Para entregarle a tus usuarios iOS / Android / WebPush vas a necesitar siempre un servidor que aloje la Firebase service account y las VAPID keys. Ese servidor no es Podio.

  3. Entrega de OTP por WhatsApp

    La integración con Evolution API, el cooldown de reintento, la ventana de resend de 30 segundos, y el tope de 5 intentos son hechos a la medida. Podio no tiene SMS, WhatsApp, ni un canal confiable de email-OTP.

  4. Chunked uploads resumables con SHA-256

    El pipeline de medios del chat depende de lib/chunked-upload.ts y lib/upload-resume.ts con verificación SHA-256 per-chunk y de archivo completo. La API Files de Podio es one-shot, máximo 100 MB, sin resume.

  5. Compute de medios server-side

    Transcodificación con ffmpeg, generación de blurhash, transcripción con Whisper, ProcessVideoJob, GenerateThumbnailJob. Podio no tiene ninguna primitiva de compute.

  6. Almacenamiento particionado time-series

    chat_messages (TO_DAYS RANGE) y device_logs están particionados mensualmente. Podio Items no tiene equivalente y degrada bastante antes de 500 000 filas por app.

  7. Búsqueda full-text a escala de chat

    FTS5 local + shadow FULLTEXT entrega hoy sub-segundo a través de toda la historia. La API search de Podio es clase rate-limited (250/hr) y no tiene mirror offline.

  8. Dedup con Idempotency-Key y ledger de una hora

    Cache::lock + UNIQUE composite + ventana de re-fetch de 1 h. Podio no tiene primitiva de dedup ni por header ni por columna.

  9. Pipeline de OpenAI Vision + parsers PDF

    15 parsers especializados, ImageValidation, OpenAI Vision para validar cédulas, MepFirmaVerificationService. Ninguno portable a una plataforma no-code.

  10. Queue workers propios con semántica de retry

    Queues separadas realtime, push, transcription, default; tries=3, retryAfter(), exponential backoff. Workflow Automation no tiene queues — tiene triggers.

  11. Recursión del árbol de reps multi-tenant

    Person::allDescendantLeadIds() recorre un árbol de representantes de profundidad arbitraria. Los relationship fields de Podio topan en 250 entradas por field y no tienen primitiva de query recursiva.

  12. Mantenimiento cron-driven

    Creación de particiones, backfill de FT-shadow, snapshots de KPI, premios de leaderboard, auto-advance de MEP. Workflow Automation de Podio tiene triggers pero no tiene cron nativo.

  13. Denormalización mantenida por observers

    ChatMessageObserver mantiene opportunities.last_message_*. Replicar esto en Podio costaría un webhook + una llamada API de update inversa por mensaje — duplicando el consumo de rate limit.

  14. Relaciones polimórficas

    chat_read_receipts.reader_type ∈ user|person. Los Items no hacen polimorfismo limpio.

  15. Modelo de bearer Sanctum / api_session

    El bearer de la app móvil lo emite tu capa Laravel y actúa por el lead. Podio usa OAuth 2 con autenticación per-user; convertirlo a "Podio es la auth source" requeriría flujos OAuth que la app móvil no tiene hoy.

Una plataforma sin chunked uploads, sin SDK de push móvil, sin primitiva de dedup, y con un techo de 1 000-calls-per-user no es — diga lo que diga el marketing — un Backend de chat. Conclusión de la auditoría

§ 05Lo que sí entra

La lista honesta. Estas son las superficies donde las primitivas de Podio se alinean con el trabajo, los volúmenes son lo suficientemente bajos como para que 1 000/hr sea cómodo, y la falta de real-time móvil es aceptable.

  • Vista CRM agent-facing de leads, opportunities, casos. Un Podio item por opportunity, segmentado en views por vertical (CCSS, MEP) y status. El staff usa la propia UI web de Podio; el móvil no la ve. Sync desde DSF3 vía un SyncOpportunityToPodioJob (ya tenés SyncOpportunityToErpJob — copiá el scaffolding).
  • Repositorio de documentos. PDFs generados por Gotenberg, escaneos de cédula validados por AI, formularios de reclamo MEP — adjuntos al item de Podio correspondiente. ≤100 MB cada uno, ≤200 por item. Adecuado.
  • Listas de tareas del staff (no las tareas lead-side). Podio Tasks nativos para workflow entre colaboradores. No es la misma superficie que /mis-tareas.
  • Automatización de cambios de status entre staff y herramientas. "Cuando opportunity → firmado, mensaje a Slack + crear carpeta en Drive." Workflow Automation lo resuelve trivialmente.
  • Audit log / activity stream para staff. El stream de Podio cubre auditoría low-volume perfectamente.
  • Formularios externos de intake. Webforms para abogados, testigos, terceros — sin construir UI custom.
  • Comments y @-mentions del staff sobre un caso. Los comments de item (1 000 char cada uno, 2 500 máx.) están muy por arriba del uso real del staff.

Modelo mental: Podio es el mirror CRM staff-side. DSF3 es el system of record + la API consumer-facing + el Backend pesado.

§ 06La matemática del rate limit

Suponé que ignorás cada advertencia anterior y querés meter el chat por Podio. Un modelo de carga al ojo: un lead moderadamente activo manda 8 mensajes, hace scroll por 4 tareas, abre 2 casos, toca una notificación — llamémoslo 30 operaciones API en una sesión enfocada de 15 minutos. En pico podría haber 150 de estos leads simultáneos. El agregado anda bien. El techo per-user no.

Fig. 3 Techo de calls-per-hour en tres plataformas, en escala log. El 1 000/hr per-user de Podio (250/hr en calls resource-intensive) queda cuatro órdenes de magnitud por debajo de lo que una sesión de chat impulsada por Pusher consume normalmente — aunque el conteo de Pusher no es un conteo de "calls" en el mismo sentido, la barra de la derecha se lee como "el piso por debajo del cual no te chocás con una pared".

La molestia que se acumula: cada attachment de chat cuesta dos calls API de Podio (upload → attach). Cada subscribe de CometD en una conversación nueva cuesta una call extra para traer la signature del canal. Cada webhook que aterriza te obliga a traer el item modificado para ver qué cambió — los webhooks de Podio cargan IDs, no bodies. El factor de amplificación entre "cosas que hizo el usuario" y "calls API a Podio" se sitúa entre 2× y 4×.

Un lead ruidoso — heavy uploader, fast typer — pega contra su techo per-user de 1 000/hr y le mandan 420 mientras todo el mundo anda bien. No podés subir el techo per-user a posteriori. Tendrías que abrir un ticket de soporte de Progress por cada cuenta bloqueada.

§ 07La matemática del costo

El castigo real no es la latencia, es el pricing per-user. Podio vende por cuenta paga de usuario de Podio. Para usar Conversations nativamente, los leads tienen que ser usuarios de Podio. Incluso si usás tokens de app-authentication (un solo usuario compartido haciéndose pasar por todos los leads), el rate limit es "per user per API key" — un solo bucket de 1 000/hr para toda la base de clientes.

Fig. 4 Costo mensual de infraestructura en cuatro escenarios. DSF3 hoy corre sobre dos VPS (190.112.223.9 + 168.197.96.20), MySQL, Caddy, supervisor. Los escenarios de Podio asumen 2 000 leads activos convertidos en cuentas pagas de Podio; staff-only asume 15 seats agent pagos y los leads se quedan en DSF3.
Reality check La barra de "Staff-only Premium" — $360/mes por 15 seats agent pagos — es el único escenario Podio-on-Podio cuyos números no terminan en una factura mensual de cinco cifras. Ese, no por casualidad, es el scope que la Arquitectura A en la próxima sección efectivamente recomienda.

§ 08Cuatro arquitecturas

Con todo lo que vimos en §§01–07, estas son las formas realistas de integrar Podio sin romper la app móvil. La primera es lo que hay que hacer. La cuarta es lo que no.

Arquitectura B · Viable pero comprometida

Podio para metadatos low-volume, DSF3 para el resto

Movés solo perfil y los metadatos ligeros de tareas / casos a Podio Items. Chat, push, OTP, medios se quedan en DSF3. Los read paths del móvil quedan: chat / medios / uploads → DSF3; perfil / tareas / casos → proxy DSF3 → Podio. El proxy ratelimita, cachea, y hace circuit-break para que los 420s de Podio nunca le lleguen al dispositivo.

Costo
~$700–1 200/mes
Riesgo
Medio — uptime ⊆ SLA de Podio
Impacto móvil
Ninguno a nivel de contrato
Capacidad perdida
Real-time en tareas/casos se vuelve latencia de webhook

Arquitectura C · Opción pegamento

Podio + orquestación con Make / Workato / Zapier

Algunos equipos usan Make.com o Workato como capa de orquestación que se dispara de los hooks de Podio y corre lo pesado (OpenAI Vision por API, ffmpeg vía Cloudconvert, push vía FCM HTTPS). Funciona para workflows low-volume, no-time-critical. Para cualquier cosa con forma de chat o con target de latencia sub-segundo es demasiado lento — la cadencia free de triggers de Make es 15 minutos; tendrías que pagar premium por webhooks a nivel de segundo.

Costo
$50–500/mes encima de Podio
Riesgo
Medio — vendor extra + debugging opaco
Impacto móvil
Ninguno directamente
Capacidad perdida
Latencia en automatización; visibilidad de queue depth

Arquitectura D · No recomendada

Podio como primario

Para hacer de Podio el system of record necesitarías: un seat pago de Podio por lead (o un único usuario de impersonation con límites elevados por ticket); un sidecar que emita tokens FCM / APNs; un sidecar que corra ffmpeg, Whisper, blurhash; un sidecar que firme tokens de LiveKit; un sidecar que corra los cálculos de KPI; un sidecar que mande OTP por Evolution.

Terminarías con más infraestructura de la que tenés hoy, más el techo de rate de Podio encima, más una relación de vendor con Progress Software cuyo rumbo es incierto. No lo hagás.

Costo
$28k–$48k/mes (2 000 leads)
Riesgo
Alto — vendor + rate + uptime + costo
Impacto móvil
Flujos OAuth, reescrituras de la app
Capacidad perdida
Chat, push, OTP, medios — todos necesitan sidecars aparte igual
La trampa a evitar es "movemos el chat a Podio Conversations nomás". Hacé el cálculo de rate limit per-user y de pricing per-seat, y la cuenta se cierra sola. Revisión de arquitectura

§ 09La Arquitectura A en detalle

Si la Arquitectura A es la que se adopta — y los datos de §§01–08 apuntan ahí — vale la pena examinarla con la profundidad que el resto del informe le dedicó a descartar las otras tres. Esta sección la desarma: qué vive dónde, cómo se mueven los datos, qué pasa cuando algo falla, y en qué orden la implementás.

El principio que la organiza

Una sola regla con dos lecturas:

  • DSF3 es el system of record. Cada lead, cada opportunity, cada mensaje, cada documento se origina y persiste en MySQL primero. Si Podio desaparece mañana, no perdiste nada.
  • Podio es un mirror, no una fuente. El staff vive ahí porque la UI no-code de Podio es donde son productivos. La data está ahí también, no en cambio.

Esto te compra dos cosas que ninguna de las otras arquitecturas te da: vendor risk acotado (Progress Software puede cambiar de rumbo sin que tu producto se rompa) y mobile risk cero (la app móvil no se entera de que Podio existe).

Qué vive dónde

Una vista por entidad para que no quede ambigüedad:

Entidad DSF3 (system of record) Podio (mirror) Dirección de sync
Lead / Person Tabla persons con TSE, security questions, push tokens, terceros Item en app "Leads" con cédula + nombre + vertical principal + status DSF3 → Podio
Opportunity Tabla opportunities con denorm (last_message_*, unread_lead_count) Item en app "Oportunidades" con campo Lead-reference, vertical, status, current_step DSF3 → Podio
Caso / CaseFile Modelo CaseFile con progreso de step, fecha de firma, tipo de cierre Item en app "Casos" con Opportunity-reference y step actual DSF3 → Podio
Chat messages Tabla chat_messages particionada Nada. Quizás un snippet del último mensaje en el item Opportunity DSF3 → Podio (snippet)
Documents / PDFs Storage local + Caddy, paths inmutables en chat_message_attachments u opportunity_documents File adjunto en el item correspondiente (≤100 MB) DSF3 → Podio
Notas / comentarios del staff Comments en el item de Opportunity / Caso Podio → DSF3 (via webhook)
Status workflow (firmado / cerrado / etc.) Field opportunities.status Field categoría en el item; Workflow Automation reacciona al cambio Bidireccional
KPI / Telemetry Tablas device_logs, kpi_daily_snapshots Nada — Podio no puede absorber este volumen No sync
Push tokens / FCM Tabla person_push_tokens Nada — Podio no manda push al móvil No sync

El mapa del sistema

Visto de afuera, el sistema en Arquitectura A se ve así. El staff y los leads viven en mundos físicamente separados; DSF3 es el único punto que toca a ambos.

MUNDO DEL LEAD App Móvil Expo · iOS · Android · PWA SYSTEM OF RECORD DSF3 Backend Laravel · PHP 8.4 · MySQL · Caddy chat_messages opportunities queue: realtime queue: push ffmpeg · Whisper OpenAI Vision SyncToPodioJob (nueva) observers cron jobs MUNDO DEL STAFF Podio CRM Items · Comments Workflow Auto. (mirror, no source) ≤ 20 staff seats 1 000 calls/hr techo Pusher HTTPS Pusher WS webhook async · idempotente callback FCM · APNs WebPush · VAPID Evolution WhatsApp OTP OpenAI · Whisper Vision · transcript.

Ilus. 1 Arquitectura A vista de afuera. La app móvil habla solo con DSF3 (HTTPS para writes, Pusher para reads en tiempo real). DSF3 espejea hacia Podio de forma asíncrona vía un job nuevo (SyncToPodioJob); los webhooks de Podio regresan a DSF3 cuando el staff edita algo. Los servicios externos (FCM, Evolution para WhatsApp, OpenAI Vision) los sigue invocando DSF3 — Podio nunca los toca.

El ciclo de un mensaje

El sequence diagram más útil de la arquitectura es lo que pasa cuando un lead manda un mensaje al agente. El lead recibe el ack instantáneo via Pusher; el mirror de Podio aterriza unos cientos de milisegundos después, de forma asíncrona, sin bloquear nada.

LEAD MÓVIL DSF3 LARAVEL PUSHER PODIO CRM POST /lead-chat/messages + Idempotency-Key: ulid() INSERT chat_messages broadcast(MessageSent) envelope ≤ 400 bytes message.sent envelope dispatch SyncToPodioJob 201 Created · full message commitMessage → SQLite ~ 200ms · ASYNC ↓ POST /item/{oppItem}/comment o update field "last_message_snippet" 200 OK emit sync_completed

Ilus. 2 El ciclo de un mensaje en Arquitectura A. Los pasos 1–4 son el camino rápido (≤ 200 ms al lead vía Pusher). El paso 5 dispara SyncToPodioJob en la queue, que aterriza en Podio asíncronamente — falle o no, el lead ya vio su mensaje confirmado. La línea naranja es el límite entre lo crítico (síncrono, mobile-visible) y lo eventual (asíncrono, staff-visible).

Mapeo de datos: Opportunity → Podio Item

El sync no es una serialización ciega. Cada field tiene un mapping intencional, y algunos campos de DSF3 directamente no se mueven. Esta es la única traducción que vale la pena hacer detallada — los demás models siguen el mismo patrón.

DSF3 MYSQL · opportunities Opportunity idBIGINT PK lead_idFK persons verticalENUM(ccss,mep) statusVARCHAR current_stepSMALLINT assigned_to_idFK users last_message_atDATETIME last_message_snippetVARCHAR(255) unread_lead_countINT unread_agent_countINT NO SE ESPEJEA created_at, updated_at (no útil en CRM) idempotency_key (interno) tareas (relación) — su propio mirror chat_messages (relación) — solo snippet device_logs (telemetry) — vol. excede Podio PODIO · APP "OPORTUNIDADES" Item External IDtext "opp-{id}" Leadcontact ref Verticalcategory Statuscategory Step actualnumber Asignado amember Última actividaddate Último mensajetext Sin leer · leadnumber Sin leer · agentenumber ÚNICO EN PODIO Comments del staff (1 000 char ea.) Tags del workspace Documentos adjuntos (≤200, ≤100 MB) Activity stream (audit)

Ilus. 3 Mapeo field-por-field entre la tabla MySQL opportunities y el item de Podio en la app "Oportunidades". El External ID es la pieza clave — siempre "opp-{id}" derivado del primary key de DSF3 — porque permite que el sync sea idempotente: hacer upsert by external_id en lugar de create, evitando duplicados aunque el job corra dos veces. Lo de abajo a la izquierda no se espejea (volumen, ruido, o sensibilidad). Lo de abajo a la derecha es contenido nacido en Podio que regresa a DSF3 vía webhook.

El job pattern

El sync job sigue al pie de la letra el patrón de SyncOpportunityToErpJob que ya existe — eso te lo evita reinventar. Lo único nuevo es el cliente PodioClient (un wrapper delgado sobre la API REST de Podio con retry, rate-limit handling, y refresh de OAuth token).

app/Jobs/Podio/SyncOpportunityToPodioJob.php

namespace App\Jobs\Podio;

use App\Models\Opportunity;
use App\Services\Podio\PodioClient;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class SyncOpportunityToPodioJob implements ShouldQueue
{
    use Dispatchable, Queueable;

    public int $tries = 5;
    public int $backoff = 60; // segundos; 60 · 120 · 240 · 480 · 600

    public function __construct(public int $opportunityId) {}

    public function handle(PodioClient $podio): void
    {
        $opp = Opportunity::with('lead', 'caseFile')
            ->findOrFail($this->opportunityId);

        // Upsert idempotente — Podio busca por external_id antes de crear.
        $podio->upsertItem(
            appId:      config('podio.opportunity_app_id'),
            externalId: "opp-{$opp->id}",
            fields: [
                'lead_cedula'      => $opp->lead->cedula,
                'lead_name'        => $opp->lead->full_name,
                'vertical'         => $opp->vertical,
                'status'           => $opp->status,
                'current_step'     => $opp->current_step,
                'last_message_at'  => $opp->last_message_at?->toIso8601String(),
                'last_msg_snippet' => $opp->last_message_snippet,
                'assigned_to_id'   => $opp->assigned_to_id,
            ],
        );
    }

    public function retryUntil(): \DateTime
    {
        // si Podio está caído >1h, dejá de pelearle al rate limit.
        return now()->addHour();
    }
}

El cliente PodioClient::upsertItem hace dos cosas que importan: (a) detecta HTTP 420 y agenda un retry con backoff exponencial, sin contar contra $tries hasta que el rate window se libere; (b) detecta el error de "external_id existe" y se cambia a updateItem automáticamente, garantizando idempotencia incluso si el job se dispara dos veces.

Webhooks de regreso

Cuando el staff edita un item en Podio — cambia el status, agrega un comment, sube un PDF — el webhook le pega a un endpoint nuevo en DSF3:

01

Podio dispara

POST /podio/webhooks/item-update con type, item_id, external_id. Sin body — solo IDs.

02

DSF3 ack inmediato

Verifica HMAC en query param, devuelve 200 OK en < 50 ms, enqueue ProcessPodioWebhookJob.

03

Reconciliación

El job hace GET /item/{id} a Podio, compara contra DSF3, aplica diff a MySQL, y broadcast opportunity.updated en Pusher.

Riesgo silencioso Si DSF3 devuelve no-2xx en 50 webhooks consecutivos, Podio auto-desverifica el hook y deja de dispararlo, sin notificar. Tenés que correr un health-check job diario que haga GET /hook/{id} y re-verifique si está caído. La memoria feedback_observer_post_link_pattern.md tiene el patrón equivalente que ya usás en DSF3 para detectar denorm drift.

Rollout por fases

Ocho semanas, cuatro fases. Cada una entrega valor staff-visible sin tocar el código de la app móvil ni una vez.

S1 S2 S3 S4 S5 S6 S7 S8 S9 FASE 0 setup FASE 1 sync opportunities + casos FASE 2 sync documentos FASE 3 webhooks Podio → DSF3 FASE 4 flows ROLLOUT · 8 SEMANAS organización PodioClient + job attach + replace verify + reconcile Slack / email

Ilus. 4 El rollout en un Gantt resumido. Las fases 2 y 3 corren en paralelo en las semanas 4–7 — son features ortogonales (uploads vs callbacks) que no comparten código. La fase 4 es la última porque depende de que el sync ya esté estable: no querés Workflow Automations disparando Slacks contra estado inconsistente.

F0 Semana 1

Setup organizacional

Crear la org de Podio, comprar 15 seats Premium, definir los workspaces (uno por vertical: CCSS, MEP). Definir el schema de las apps: Leads, Oportunidades, Casos, Documentos. Configurar OAuth + client_id, generar refresh tokens, guardarlos en .env de DSF3.

  • Schema "Oportunidades" con los 10 fields del mapping de Ilus. 3
  • Workflow Automations apagadas (todavía no)
  • Roles configurados: staff abogado · staff admin · staff KPI
F1 Semanas 2–3

Sync one-way de opportunities y casos

Escribir App\Services\Podio\PodioClient con upsertItem, updateItem, manejo de HTTP 420. Implementar SyncOpportunityToPodioJob y SyncCaseToPodioJob. Disparar desde el OpportunityObserver existente en eventos created y updated.

  • Job en queue podio-sync (dedicada, 2 workers, separada de realtime y push)
  • Telemetry: podio_sync_enqueued, podio_sync_completed, podio_sync_failed
  • Comando one-shot podio:backfill-opportunities para hacer el sync inicial sin esperar updates
F2 Semanas 4–5

Sync de documentos

Cuando se adjunta un PDF a una opportunity en DSF3 — generado por Gotenberg, escaneado por OpenAI Vision, etc. — se replica al item correspondiente de Podio. Sub-100 MB queda directo; los pocos archivos >100 MB se quedan en DSF3 con un placeholder link en Podio.

  • Job SyncDocumentToPodioJob escucha el evento OpportunityDocumentCreated
  • Skip silencioso para archivos >100 MB; staff ve un link "Ver en DSF3"
  • Telemetry: podio_document_synced, podio_document_too_large
F3 Semanas 6–7

Webhooks Podio → DSF3

Endpoint POST /podio/webhooks/item-update con verificación HMAC. ProcessPodioWebhookJob trae el item, hace diff, aplica a MySQL. Health-check diario que verifica que el hook sigue verified.

  • Status del staff → status en DSF3 (con opportunity.updated Pusher event)
  • Comments del staff → activity log de DSF3 (no se le muestran al lead — son notas internas)
  • Tests de integration: 50 hooks consecutivos failing no desactivan el endpoint en silencio
F4 Semana 8

Workflow Automation

Encendés las flows que el staff ya pidió: "cuando status → firmado, mandar Slack a #ops", "cuando se sube un PDF a Casos, crear carpeta en Drive". Mantenelas boring — la lógica compleja se queda en DSF3.

  • Slack notifications para status transitions críticas
  • Auto-asignación de nuevos casos por carga del agente (Podio puede hacerlo sin sidecar)
  • Email summary semanal a directores con conteo de items por status

Modo de falla y recuperación

Si la regla es que DSF3 es el system of record, entonces todos los modos de falla del lado de Podio son recuperables. Ninguno es data-loss. Esa es la diferencia que justifica esta arquitectura.

Falla Efecto visible Recuperación
Podio API responde 420 (rate limit) Sync queue acumula items pendientes Job retries con backoff exponencial. Si la cola crece >500, alerta de PagerDuty. podio:backfill-opportunities --since=YYYY-MM-DD rehace todo si querés.
Podio está caído 4 horas Staff no puede ver updates nuevos Mobile app no se entera. Cuando Podio vuelve, la queue se drena. Los webhooks de Podio se reintentan automáticamente cuando recupera. Nada se pierde.
OAuth token expira Todos los SyncToPodioJob fallan con 401 PodioClient detecta 401, refresca el token via refresh_token almacenado, reintenta una vez. Si el refresh también falla, alerta + manual re-auth desde panel admin.
Webhook auto-unverified después de 50 fallos Updates del staff no llegan a DSF3 Health-check diario detecta el hook inactivo y lo re-verifica automáticamente. Mientras tanto, no perdés data — el staff puede esperar; o corrés podio:reconcile-from-podio --opp={id}.
External ID duplicado en Podio Dos items para la misma opportunity El upsert idempotente lo previene por construcción. Si igual pasa (corrupción manual), el comando podio:dedupe-items --app=oportunidades los fusiona conservando el primero.
Progress decide deprecar Podio Plataforma desaparece en N meses DSF3 sigue funcionando exactamente igual sin Podio. El staff vuelve a usar la CRM web propia de DSF3 (que es lo que usan hoy de todos modos). El proyecto Podio se apaga sin pánico.
El staff vive en Podio porque les es productivo. La app móvil vive en DSF3 porque tiene que serlo. Y si mañana Podio cierra, el negocio sigue. Tesis de la Arquitectura A

Lo que esta arquitectura no resuelve (y no debería)

Es importante reconocer dónde Arquitectura A se queda corta:

  • El staff sigue teniendo dos UIs. Mientras DSF3 tenga su propia CRM web (la que viven en dsf3/src), el staff puede preferir esa para algunos workflows y Podio para otros. Resolver esto requiere decidir cuál es la UI canónica y deprecar la otra.
  • El sync introduce latencia de ~1–5 segundos. Un agente que ve un cambio en Podio puede que vea el state un par de segundos atrasado vs lo que el lead acaba de hacer. Aceptable para CRM, no para chat — por eso el chat queda fuera del sync.
  • Workflow Automation no es Laravel. Cualquier flow que se vuelva complejo va a tener que migrarse a un App\Listeners\* en DSF3 igual. Mantenerla simple es un compromiso disciplinario.
  • El costo de Podio es ~$360/mes recurrente. Si el staff no termina usándolo (como pasó con la "Dedicated messaging app" thread del forum de Podio en 2019), ese dinero está tirado. Hacé un pilot de 60 días antes de comprometer 15 seats anuales.

La Arquitectura A es la respuesta correcta a una pregunta intermedia: "¿cómo le damos al staff un workspace no-code sin romper la app móvil?" No es la respuesta a "¿cómo movemos DSF a Podio?" — esa pregunta, vista desde acá, no tiene una respuesta sensata.

§ 10Esencial vs deseable

Si una migración tuviera que ocurrir sí o sí, ¿qué se puede dar el lujo de perder la app móvil? Los tiers de abajo son míos; defendelos o reemplazálos, pero el orden de prioridad es honesto.

TierFeaturePor qué cae acá¿Podio?
P0Chat real-time < 1 sEl producto es "hablá con tu abogado"No
P0Push notificationsSin ellas la app muere en backgroundNo
P0OTP por WhatsApp al registrarseEl público lead tico lo esperaNo
P0Outbox de chat offline + gap-fillRealidad de la red móvil en CRNo
P0TSE lookup al registrarseYa es client-side — no requiere trabajoN/A
P0File upload ≤25 MB con progressIntake de evidencia — imagen, audio, PDFNo
P1Tareas + Casos con real-timeDrive de engagement + triggers de pushParcial
P1Encuestas (wizard CCSS / MEP)Requerido para intake del casoParcial
P1Read cacheado del perfilVelocidad de first-render (zero-skeleton)
P1Terceros (árbol de reps)Caso de uso multi-accountParcial
P2Búsqueda dentro del chatDegrada limpiamente si no estáNo
P2Voice agentFeature betaNo
P2Transcripción de audioQuality of life, no bloqueanteNo
P2Link previewsCosméticoNo
P3Telemetry analyticsPreocupación del operador, no del usuarioNo
P3Rewards / gamificaciónAspiracionalParcial
Fig. 5 Features móviles apiladas por tier de prioridad, coloreadas por viabilidad en Podio. La barra P0 es casi enteramente roja. Cualquier cosa plausible solo empieza en P1 hacia abajo — y aún ahí, el color Parcial quiere decir "viable detrás de un proxy DSF3", no "Podio lo resuelve solo".

§ 11Registro de riesgos

Si seguís con la Arquitectura A (la recomendada), o cualquier otra variante que ponga algo en Podio, estos son los riesgos operativos que vale la pena rastrear desde el día uno.

RiesgoSeveridadMitigación
Incertidumbre estratégica de Podio bajo Progress
Adquisición cerrada en Oct 2024
Alta Tratá a Podio como una capa reemplazable detrás de un adapter; nunca escribás tu única copia de data en Podio. Dual-write a DSF3 primero, después espejeás.
420 rate-limit en un mal día Alta Caching agresivo client-side, dual-write (Podio + tu propio cache), receptores de webhook idempotentes, backoff exponencial en 420.
Pérdida de dedup con Idempotency-Key en replies a Podio Alta No movás el chat. Si tenés que hacerlo, corré el dedup en tu propio proxy delante de Podio.
CometD en móvil no está probado Alta No uses el push de Podio para la app móvil. Quedate con FCM/APNs/WebPush.
1 000/hr per-user bloquea UX con bursts Alta Los clientes móviles nunca hablan directo con Podio. Pasá siempre por proxy DSF3 (o Lambda) para poder ratelimit, cachear, circuit-break.
Disable silencioso del webhook tras 50 fallos Media Health-check diario: GET /hook/{id}; alerta + re-registro automático si está disabled.
Timeout de 15 s en webhook Media Devolvé 200-OK rápido y enqueue al queue worker. Nunca hagás trabajo real dentro del handler del webhook.
Archivos > 100 MB Media Dejá el video pipeline de tu lado. Espejeá solo thumbnails / metadata a Podio.
Rechazo de application/octet-stream en RN Android Media Verificá MIME server-side (libmagic) antes de reenviar a Podio. Memoria: feedback_rn_android_strips_content_encoding.md.
Sin payload signing en los hooks de Podio Media Firmá la callback URL con HMAC en query param. Rotá el secret trimestralmente. Validá antes de procesar.
Profundidad del árbol de reps > 250 nodos por rama Baja Hoy improbable. Si llega a pasar, el árbol de reps se queda en DSF3 — solo aplaná los roots a Podio.
Cost creep de seats de Podio Baja Capá los seats al staff. La identidad lead-side se queda en DSF3 para siempre.

§ 12Recomendación y cierre

  1. No movás chat, push, OTP, procesamiento de medios, orquestación de reclamo MEP, telemetry, ni cálculo de KPI a Podio. Cada uno tiene una razón estructural por la que Podio no puede hostearlo. La auditoría encontró que el Backend es estructuralmente un sistema consumer-facing de alto throughput con particionado, observers, queues, ffmpeg, y OpenAI Vision — ninguno con análogo en Podio.
  2. Adoptá la Arquitectura A (DSF3 primario, Podio como mirror CRM staff-side). Usá Podio para lo que es realmente bueno: un workspace no-code agent-facing donde ops puede ver leads, status, PDFs adjuntos, y agregar comments. Sync de DSF3 a Podio vía un SyncOpportunityToPodioJob siguiendo el patrón existente de SyncOpportunityToErpJob.
  3. TSE lookup ya está hecho. Client-side fetch("https://www.dsf.cr/tse/{cedula}"). No requiere trabajo independientemente de a dónde vaya el resto.
  4. Si los stakeholders empujan por "Podio como primario", vendéles la Arquitectura A como cobertura de lo que realmente quieren: una UI que el staff puede configurar sin engineering. La app móvil no cambia.
  5. Planeá para el vendor risk. El compromiso de Progress con Podio no está probado en esta etapa. Hacé Podio reemplazable detrás de un adapter; no dejés que se vuelva la única copia de nada.
  6. Independientemente de Podio, considerá estas mejoras de DSF3:
    • Mové ffmpeg / Whisper / blurhash a un worker pool aparte o a AWS MediaConvert + Lambda — las queues realtime y push se benefician si el compute de medios se sale de la misma caja.
    • Considerá Cloudflare R2 + signed URLs para los medios del chat en lugar de disco local del VPS + Caddy.
    • El sampler del 1 % de chat_query_partitions_scanned en AppServiceProvider::boot es el ejemplo canónico de por qué tu propio stack vale la pena — tenés observabilidad de tu propia DB. Eso no lo tenés en Podio.

La pregunta correcta no es "¿podemos mover features a Podio?". Es "¿qué rol queremos que juegue Podio?". La respuesta a la que apuntan los datos es: mirror staff-side, mantenido en sync vía webhooks, mantenido reemplazable detrás de un adapter, pagado a escala staff-only.