Cristian Carreño

Marketplace de cuidado de mascotas en Chile

Snup

Un marketplace de cuidado de mascotas en Chile, hecho por un ingeniero y sus agentes: 826 commits y 145 funcionalidades nuevas en los últimos 30 días.

Rol
Fundador · único ingeniero desde marzo de 2025
Período
2023 – hoy
Estado
En producción
Código
Privado
commits en los últimos 30 días, todos coescritos con Claude
826
funcionalidades nuevas en esos 30 días
145
versiones en esos 30 días
61
commits desde marzo de 2025, todos míos
3.779
  • Elixir
  • Phoenix
  • LiveView
  • React
  • TypeScript
  • MongoDB
  • Auth0
  • Expo

01 Contexto

Por qué existe

Snup conecta a tutores de mascotas con cuidadores verificados para alojamiento, guardería, paseos, visitas y cuidado a domicilio. El marketplace es solo la mitad. La otra mitad es operación: revisar las postulaciones de cuidadores contra un checklist, una bandeja de soporte, broadcasts, pagos a cuidadores, políticas de cancelación que elige cada cuidador y un ranking con advertencias. Por eso la app de administración es un producto en sí mismo, con su propio bundle, service worker y tren de versiones.

El código llegó como una app Phoenix existente (el namespace Cuidog sigue ahí), importada el 1 de marzo de 2025. El stack anterior de Snup, apps React Native sobre una API NestJS, se había estancado. Desde esa importación soy el único ingeniero: producto, arquitectura, versiones y producción.

02 En uso

Qué hace

Cinco pantallas de las apps reales, compiladas desde el repositorio y llenas de personas, mascotas y mensajes inventados. Las tres primeras son la app que usan tutores y cuidadores en el teléfono; las dos últimas, la app de administración.

El chat es lo que más atención recibe. Es donde tutores y cuidadores acuerdan una estadía, y lo rehice desde cero en 2026: más de 140 entradas del CHANGELOG desde agosto son sobre él.

El perfil de un cuidador en la app de Snup: un perro ilustrado como avatar, nombre, comuna, nota 4,9 con 12 reseñas, insignias de perros y gatos aprobados en la formación de Snup y un botón para contactarlo. El perfil de un cuidador en la app de Snup: un perro ilustrado como avatar, nombre, comuna, nota 4,9 con 12 reseñas, insignias de perros y gatos aprobados en la formación de Snup y un botón para contactarlo.
Datos simulados El perfil público de un cuidador visto por un tutor: antecedentes verificados, especies aprobadas en la formación de Snup, reseñas y un botón para contactarlo.
Un chat entre una tutora y un cuidador: el cuidador pregunta por la rutina de la perrita, la tutora responde con un mensaje de voz de 17 segundos que se muestra con su transcripción, y después responde un mensaje anterior sobre el jueves. Los mensajes tienen reacciones y marcas de leído, y la cabecera tiene un botón Reservar. Un chat entre una tutora y un cuidador: el cuidador pregunta por la rutina de la perrita, la tutora responde con un mensaje de voz de 17 segundos que se muestra con su transcripción, y después responde un mensaje anterior sobre el jueves. Los mensajes tienen reacciones y marcas de leído, y la cabecera tiene un botón Reservar.
Datos simulados Una tutora y un cuidador coordinan una visita: un mensaje de voz con su transcripción (muy pronto), la respuesta a un mensaje anterior, reacciones, marcas de leído y el botón Reservar en la cabecera.
Una solicitud de cuidado abierta por cuatro noches con dos mascotas, en el paso 3 de 4, con tres cuidadores que se ofrecieron, opciones para ordenarlos y la primera oferta. Una solicitud de cuidado abierta por cuatro noches con dos mascotas, en el paso 3 de 4, con tres cuidadores que se ofrecieron, opciones para ordenarlos y la primera oferta.
Datos simulados Una solicitud abierta de una tutora: cuidadores cercanos se ofrecieron, y ella los compara, ordena y conversa con ellos antes de pagar.
La app de administración revisando una postulación de cuidador: acciones para aprobar o rechazar, el quiz de perros aprobado 4 de 4 y un checklist de revisión con un ítem rechazado y la sugerencia de pedir una corrección. La app de administración revisando una postulación de cuidador: acciones para aprobar o rechazar, el quiz de perros aprobado 4 de 4 y un checklist de revisión con un ítem rechazado y la sugerencia de pedir una corrección.
Datos simulados Revisión de una postulación de cuidador: los chequeos automáticos llenan parte del checklist, un admin marca el resto y el panel sugiere pedir una corrección.
Las notas de versión en la app de administración, con filtro App o Admin, una tarjeta para la versión que sigue en pruebas y los cambios de la última versión agrupados en nuevo, seguridad y cambios. Las notas de versión en la app de administración, con filtro App o Admin, una tarjeta para la versión que sigue en pruebas y los cambios de la última versión agrupados en nuevo, seguridad y cambios.
Datos simulados Las notas de versión dentro del panel, tomadas del CHANGELOG, con lo que sigue en pruebas separado de lo que corre en producción.

Lo que hace el chat

  • Mensajes de voz, muy pronto

    Se mantiene presionado para grabar hasta un minuto, y se desliza para cancelar o hacia arriba para seguir grabando sin sostener el botón. Cada nota se transcribe y se modera como el texto antes de que alguien la escuche, y se reproduce hasta a 2×.

  • Fotos, videos y archivos

    Desde la cámara o varios a la vez, con un anillo de progreso, cancelar y reintentar, y un visor a pantalla completa.

  • Reacciones y respuestas

    Ocho reacciones rápidas o el selector completo, y basta deslizar un mensaje para responderlo.

  • Editar y borrar

    Dentro de 15 minutos, con las ediciones moderadas de nuevo, u ocultar un mensaje solo para ti.

  • Leído, escribiendo y en línea

    Las marcas de entregado y leído, como las de WhatsApp, «escribiendo…» y «visto hace…». Un mensaje cuenta como leído solo si estuvo en pantalla.

  • Funciona sin conexión

    Una conversación se abre al instante desde la copia que guarda el teléfono, y los mensajes escritos sin conexión quedan en cola y se reintentan.

  • Búsqueda, filtros, archivar y silenciar

    Buscar por contenido, ver los no leídos o las conversaciones con reserva, deslizar para archivar y silenciar por cuatro horas, ocho horas o una semana.

  • Reservas dentro del chat

    Un botón Reservar en la cabecera, y los mensajes de reserva como tarjetas con su estado actual.

  • Los datos de contacto quedan protegidos

    Teléfonos, correos y cuentas de redes sociales se bloquean hasta que la reserva está pagada, con un aviso mientras escribes. Los enlaces externos solo funcionan si los escribió el equipo de Snup.

  • El lado de administración

    Tres columnas: las conversaciones, el chat y su contexto. Un admin puede leer cualquier conversación, entrar a ella como asistente de Snup y revisar los mensajes bloqueados.

03 Arquitectura

Cómo funciona

Todo sale en un solo release de Phoenix. El router lo divide en pipelines: un sitio de marketing en LiveView con sus propios bundles, dos apps React de una sola página servidas dentro de shells HTML que renderiza el servidor, APIs JSON para cada una y el endpoint MCP. Los datos viven en MongoDB a través del driver, sin esquemas.

Arquitectura de Snup Un release, muchas superficies. La app para las tiendas envuelve la PWA; los admins también llegan a Snup desde Claude por MCP; el trabajo en segundo plano corre en colas guardadas en MongoDB. Clientes Un release de Phoenix Datos y servicios App para tiendas Expo, en desarrollo Apps web PWA React y admin Claude MCP, solo admins Router + shells LiveView, HTML de SPA APIs JSON app, admin, MCP Channels diffs del chat en vivo Colas en Mongo reenvíos, chat, hooks MongoDB Atlas driver, sin esquemas Proveedores auth, pago, mail, push envuelve la PWA carga el shell JSON + cookie WebSocket MCP + OAuth vía el driver envíos reintentados
Un release, muchas superficies. La app para las tiendas envuelve la PWA; los admins también llegan a Snup desde Claude por MCP; el trabajo en segundo plano corre en colas guardadas en MongoDB.
  1. Un release, dividido por pipeline

    Las landings están hechas en LiveView. /app y /admin-app son apps React dentro de shells que renderiza el servidor, así que cada ruta del cliente también necesita una ruta en Phoenix. El archivo de reglas lo marca como obligatorio, y la app de administración mantiene un catch-all como respaldo.

  2. Chat en tiempo real, local-first

    Los Phoenix Channels envían los cambios; la PWA guarda el historial completo de cada sala en IndexedDB y se pone al día con un solo endpoint de diffs. La vista de administración deja a propósito que el servidor sea la fuente de verdad.

  3. MongoDB sin esquemas, sostenido por código

    No hay una capa de esquemas: 49 colecciones de documentos simples, y la mayoría de las llamadas al driver pasan por 45 módulos de repositorio. Lo que mantiene los datos en orden es código que corre sin que nadie tenga que acordarse. Cada índice se declara junto a la consulta que atiende, con un modo de revisión que informa lo que falta, y los más nuevos se crean cuando arranca la app. Los índices únicos cubren las carreras que el código no puede evitar, como un webhook que llega dos veces o dos mascotas activas con el mismo nombre. Los ids que llegan como ObjectId o como texto pasan por un solo normalizador, y un solo módulo sabe dónde viven el nombre, el teléfono y la dirección de una persona.

  4. Trabajo en segundo plano sobre MongoDB

    Cinco colas guardadas en Mongo, cada una procesada por un GenServer que hace polling, entre ellas los trabajos salientes, los broadcasts y el espejo del chat. Tomar un trabajo es atómico y el bloqueo expira si una instancia muere. Los trabajos programados corren en Quantum.

  5. La app móvil, como shell

    Una app Expo carga la PWA en un WebView. El inicio de sesión corre en el navegador del sistema con PKCE, y luego un código de un solo uso deja la cookie de sesión normal dentro del WebView. Se está preparando para las tiendas.

  6. Dos trenes de versiones

    La app y la app de administración se versionan por separado, cada una con su propio service worker. El CHANGELOG se parsea para generar las Novedades de la app, y cada entrada tiene que nombrar la app a la que pertenece; una prueba falla si alguna no lo hace.

04 Decisiones

Las decisiones que le dieron forma

  1. Decisión 01

    Mantener sincronizado el chat nuevo con un log de eventos persistido

    Elegí
    Cada escritura en el chat antiguo inserta un evento; un worker lo aplica al chat nuevo cada cinco segundos con un mapper compartido. El paso al chat nuevo fue un cambio de configuración.
    En vez de
    Un espejo síncrono inline (se probó primero y bloqueó el handler del socket en una sala real), PubSub (se pierde con una caída) y un bus general de eventos de dominio (prematuro).
    Porque
    Sobrevive a caídas y deploys, se puede reprocesar, y cada conversación se pudo revisar mensaje por mensaje antes del cambio.
    Costo
    El chat nuevo va unos segundos atrasado, tiene dos caminos de escritura, y tiene que existir un espejo inverso hasta que se retire el antiguo.
  2. Decisión 02

    Hacer el chat local-first

    Elegí
    El historial completo de cada sala en IndexedDB, al día gracias a un solo endpoint updated_since. En un conflicto gana el servidor.
    En vez de
    Un caché en el servidor con una página fija de mensajes, o sincronización bidireccional al estilo de RxDB.
    Porque
    Abrir una sala es instantáneo y funciona sin conexión, y una sola primitiva de diff cubre los mensajes nuevos, editados, borrados y con reacciones.
    Costo
    Un store en el cliente que mantener (cuota, cursores), y las ediciones en salas que no has abierto se reconcilian solo cuando las abres.
  3. Decisión 03

    Reintentar a los proveedores externos desde una cola, pero intentar primero

    Elegí
    Enviar de inmediato y encolar solo los fallos transitorios, con backoff de un minuto a seis horas. Los fallos permanentes se guardan y se muestran en la app de administración.
    En vez de
    Oban, que necesita Postgres; reintentar en memoria, lo que se pierde con cada deploy; y escribir todo primero en el outbox, lo que retrasa cada correo.
    Porque
    La mayoría de los envíos funciona al primer intento, así que la cola solo lleva los que lo necesitan.
    Costo
    De vez en cuando un correo puede llegar dos veces.
  4. Decisión 04

    Publicar la app móvil como shell WebView sobre la PWA

    Elegí
    Una sola interfaz. Cada deploy de Phoenix es también una actualización de la app.
    En vez de
    Una app nativa completa (estimada en cuatro a seis meses, y las dos apps nativas anteriores murieron así), Capacitor y una Trusted Web Activity.
    Porque
    Un solo ingeniero no puede mantener dos interfaces a la par, y la PWA ya tenía push, funcionaba sin conexión y se podía instalar.
    Costo
    Algo de riesgo de que las tiendas la rechacen, un puente de mensajes que es un contrato entre dos repositorios, y una cookie del WebView que el servidor no puede revocar directamente.
  5. Decisión 05

    Crear los índices cuando arranca la app

    Elegí
    Cada índice vive en el código junto a las consultas que lo usan, y una tarea de una sola vez lo crea al arrancar la app. Una tarea de Mix con --check informa cualquier desfase.
    En vez de
    Scripts de índices que se corren a mano antes de una versión. Así llegaron las solicitudes de cuidado a producción sin su índice de ubicación, y devolvieron errores.
    Porque
    Un deploy trae sus propios índices, y nadie tiene que acordarse de un paso.
    Costo
    Corre una vez, sin reintentos: una falla solo se ve en el log, y la tarea de Mix la repite. Un índice más antiguo todavía se aplica a mano.

05 Lo interesante

Vale la pena mirarlo de cerca

  • Una sola primitiva de diff hace toda la sincronización

    Todo cambio a un mensaje actualiza updated_at: inserción, edición, borrado lógico (el tombstone se queda), ocultamiento y reacción. Una sola consulta devuelve todo lo que cambió, y el cliente hace upsert por su propio id sin fusionar nunca.

  • Un trinquete en vez de un esquema

    El nombre, el teléfono y la dirección de una persona estaban copiados en dos colecciones. Moverlos detrás de un solo módulo se hace por fases, y una prueba que lee el código falla si las lecturas directas suben, o si bajan sin que el límite baje con ellas. La cuenta ha bajado de 157 a 91.

  • Las Novedades saben qué está publicado

    La imagen se despliega sin git, así que el servidor lee la versión de los archivos del service worker que está sirviendo y la guarda en caché. Así las Novedades de la app distinguen lo publicado de lo pendiente.

  • Un ciclo de pruebas que los agentes se pueden permitir

    Mover la suite de un clúster de base de datos remoto a MongoDB en Docker la bajó de unos 7.400 segundos a 139. Es la diferencia entre que los agentes corran las pruebas y que se las salten.

  • Una nota de voz se revisa antes de existir

    Una nota grabada espera fuera de la conversación hasta que la transcripción y la moderación la aprueban. Recién ahí se vuelve un mensaje, así que nada sin revisar llega a la sincronización, a las notificaciones ni a la lista de chats.

06 Hecho con agentes

Cómo encajan los agentes

Snup se construye con Claude Code, y el repositorio está escrito para eso. El archivo de reglas es el manual de operación: prosa en español e identificadores en inglés, español neutro, el vocabulario del producto, íconos en vez de emojis, y el CHANGELOG actualizado en el mismo pull request. Cada regla trae su motivo y, muchas veces, el incidente que la originó.

Los últimos 30 días, contados desde el repositorio: 826 commits y 61 versiones, y un CHANGELOG con 145 funcionalidades nuevas, 141 cambios y 291 arreglos, a partir de 54 briefs nuevos. Snup sumó 40 commits en julio, 218 en agosto, 352 en septiembre y 754 en octubre hasta el día 26. En esas mismas semanas construí Agents Control Center y QA Console y empecé Snup Vets. Eso es lo que le da a un ingeniero correr agentes en paralelo.

El trabajo parte de un brief numerado, 105 hasta ahora. Los agentes escriben sus preguntas en la sección de aclaraciones del brief en vez de adivinar. Las decisiones de arquitectura van en ADRs, y uno de ellos dice el porqué sin rodeos: varios agentes habían optimizado cada uno la tarea que tenían delante en direcciones distintas, porque ningún documento contenía la visión.

Algunas reglas nacieron de fallas. Un barrido de modo oscuro que hizo un agente en 49 archivos dejó 40 textos con un contraste de 1,77, porque una revisión línea por línea no ve un fondo que pone un elemento padre. El barrido se revirtió, el arreglo encontró 282 clases de Tailwind que no generan CSS, y la regla quedó así: validar la interfaz corriéndola en un navegador y medir el contraste, nunca juzgarlo por el código.

Personas del equipo que no son de ingeniería también pueden hacer cambios con Claude. Una guía mantiene un lenguaje simple y termina el flujo en un pull request probado en local, y un hook bloquea en sus sesiones el push a ramas protegidas, la reescritura del historial y los merges.

Contacto

¿Quieres profundizar en Snup?

Escríbeme por LinkedIn