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
- Sitio
- snup.cl
- 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.
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.
-
Un release, dividido por pipeline
Las landings están hechas en LiveView.
/appy/admin-appson 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. -
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.
-
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.
-
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.
-
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.
-
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
-
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.
-
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.
-
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.
-
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.
-
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
--checkinforma 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.