El stack que usamos para publicar apps Android e iOS con Expo
Eduardo Leiva, Founder, Weikiu
La mayoría de los artículos sobre stacks móviles son listas de librerías. Este es distinto: es el stack de una app que tenemos realmente en producción — una app de campo que usan todos los días los choferes de una empresa de logística, en sus propios teléfonos, en lugares donde la señal va y viene.

Los requisitos eran de los aburridos, los que importan: tiene que andar en Android y en iOS, en teléfonos económicos, con mala conectividad, y tenemos que poder corregir cosas rápido sin pedirle a nadie que reinstale nada.
Esto es lo que elegimos y por qué.
Una sola base de código: Expo + React Native + TypeScript
La app es Expo sobre React Native, escrita íntegramente en TypeScript. Una sola base de código produce la app de Android y la de iOS.
Para un cliente el argumento es simple: dos bases nativas significan más o menos dos equipos, dos backlogs y dos oportunidades para que las plataformas se desincronicen. Con una sola base, una función nueva llega a todos los choferes al mismo tiempo y hay un único lugar donde arreglar un bug.
La contrapartida existe, pero es acotada. Si tu producto es un juego o depende fuerte de hardware específico de la plataforma, nativo sigue siendo la decisión correcta. Para apps de negocio — formularios, listados, fotos, documentos, cambios de estado, notificaciones — Expo alcanza, y alcanza con componentes nativos de verdad, no con una web adentro de una cáscara.
Navegación: expo-router con rutas tipadas
La navegación usa expo-router, que funciona por archivos: un archivo dentro de app/ es una pantalla. La estructura se lee como el producto:
app/
(auth)/login.tsx
(tabs)/index.tsx // viajes de hoy
(tabs)/shipments.tsx
(tabs)/remitos.tsx
(tabs)/profile.tsx
Cualquiera que haya trabajado con el App Router de Next.js se siente en casa desde el primer día, y eso importa cuando el mismo equipo también construye la parte web del producto. Activamos rutas tipadas, así un error de tipeo en una ruta es un error de compilación y no una pantalla que no hace nada.
Estilos: NativeWind
NativeWind trae los nombres de clase de Tailwind a React Native. No es una elección de moda: el mismo producto tiene un panel web hecho con Tailwind, así que el equipo escribe el mismo vocabulario de estilos de los dos lados y se mueve entre ellos sin cambiar el chip.
Autenticación pensada para el uso real
Acá es donde suelen romperse las apps de campo. Al chofer se le apaga el teléfono, lo reinicia, pierde señal en la mitad de un formulario. Las reglas que fijamos:
- La sesión se restaura antes de renderizar cualquier cosa. El splash se mantiene hasta que leímos el token del almacenamiento y verificamos que siga siendo válido, así nadie ve un parpadeo de la pantalla de login estando ya logueado.
- El token vive en almacenamiento seguro (
expo-secure-store, que es Keychain en iOS y Keystore en Android), no en almacenamiento común. - El token CSRF nunca se persiste. Se deriva en memoria cuando se restaura la sesión.
- Un único manejador global para los 401. Desde cualquier parte de la app, una petición rechazada limpia la sesión y manda al login una sola vez — en lugar de que diez pantallas inventen cada una su propia versión de esa lógica.
- Un guard de autenticación en el layout raíz. La regla de redirección vive en un solo lugar: si no está logueado y no está en login, va a login; si está logueado y está en login, va a la app.
Nada de esto es exótico. Simplemente se decide una vez, en la raíz, en lugar de improvisarse pantalla por pantalla.
Publicar arreglos: actualizaciones OTA y versión mínima
Esta combinación es la que le recomiendo a todos los clientes, porque cada mitad cubre el punto ciego de la otra.
Las actualizaciones over-the-air (expo-updates con EAS Update) nos permiten mandar cambios de JavaScript directo a las apps ya instaladas. Un texto mal escrito, una validación equivocada, un cálculo roto: minutos, sin revisión de tienda, sin que el usuario tenga que hacer nada.
Pero OTA no puede publicar un cambio que toque código nativo, ni puede obligar a nadie a reiniciar. Por eso la app además le pregunta al backend, al arrancar, cuál es la versión mínima soportada. Si la app instalada está por debajo, aparece un modal bloqueante con el link a la tienda. Ese endpoint es público — se consulta antes del login — porque el caso más peligroso es una app vieja hablando con una API nueva y escribiendo datos mal en silencio.
Juntos cubren los dos escenarios: los arreglos chicos salen solos, y el piso de versión lo controlamos desde el servidor en lugar de rogarle al usuario.
Un interruptor remoto para las funciones riesgosas
La app baja del backend un objeto de configuración chico después del login — feature flags, básicamente. Cuando lanzamos un módulo nuevo, salió detrás de un flag apagado. Lo prendimos para usuarios reales cuando el backend estuvo listo, sin ninguna publicación de por medio. Si algo hubiera salido mal, apagarlo era una llamada a la API.
Para un cliente esto vale más de lo que suena: la fecha de publicación y la fecha de lanzamiento dejan de ser la misma fecha.
Documentos: subida directa a S3 con URLs prefirmadas
Los choferes fotografían remitos y suben documentos todo el tiempo. La versión ingenua manda el archivo a tu API y la API lo reenvía al almacenamiento — o sea que tu API paga cada byte dos veces y se cae por timeout con mala conexión.
En cambio, la app le pide a la API una URL prefirmada, sube el archivo directo a S3, y recién después le avisa a la API "acá está la clave, guardá el registro". El archivo nunca pasa por nuestro servidor. Las descargas funcionan igual pero al revés, con URLs firmadas que expiran.
Enterarnos cuando algo se rompe
Sentry está integrado y habilitado solo en producción, con el muestreo de trazas bajo para que no cueste caro. En la práctica es la diferencia entre "un chofer comentó que la app se cerró alguna vez la semana pasada" y un stack trace con el modelo del teléfono, la versión de la app y la línea exacta.
Después del lanzamiento, es lo más valioso de todo el stack. También es la parte que la mayoría de los presupuestos no incluye.
Compilar y publicar: EAS
Las builds y los envíos a las tiendas corren en EAS, con perfiles separados: una build de desarrollo con el dev client, un canal interno preview para que el cliente pruebe en teléfonos reales, y production para las tiendas. Cada perfil lleva su propia URL de API, así los que prueban pegan contra el backend de staging y no pueden tocar datos productivos por accidente.
Top tip
Guardá los secretos de build en variables de entorno de EAS y no en archivos de configuración versionados. Todo lo que está en el repositorio viaja con el repositorio — incluso hasta las manos del próximo desarrollador que se sume.
Qué significa esto para vos como cliente
El resumen honesto de elegir este stack:
- Un presupuesto en lugar de dos para Android e iOS.
- Arreglos en minutos, no en colas de revisión, para todo lo que no toque código nativo.
- Un proceso de publicación que no tenés que aprender. Certificados, fichas de tienda, formularios de privacidad y respuestas a revisión son problema nuestro.
- Una app que sigue funcionando después del lanzamiento, porque el monitoreo, los feature flags y el control de versión mínima se diseñaron desde el día uno y no se agregaron después del primer incidente.
Si estás evaluando una app para tu negocio, con gusto te decimos qué implicaría — y si una app es realmente la respuesta correcta, porque a veces no lo es. Podés ver más sobre cómo desarrollamos apps móviles o directamente escribirnos y contarnos qué necesitan hacer tus usuarios desde el teléfono.