← Regresar a lecciones

    react native

  • ci-cd

  • mobile-security

  • eas-update

  • devops

  • codepush

  • ota-updates

  • hardening

OTA Updates y Hardening en React Native CLI

Entendiendo OTA Updates

Imagina que acabas de lanzar tu aplicación móvil y descubres un bug crítico. En el modelo tradicional, tendrías que corregir el código, recompilar la aplicación completa, subirla a las tiendas de aplicaciones, esperar la revisión (que puede tardar días en iOS), y luego esperar que tus usuarios actualicen manualmente. Este proceso puede tomar días o incluso semanas, durante las cuales tus usuarios están experimentando el problema.

Aquí es donde entran las actualizaciones OTA (Over-The-Air) y el hardening de la aplicación. Estas técnicas te permiten resolver problemas rápidamente mientras mantienes tu aplicación segura.

Entendiendo OTA Updates

Las actualizaciones Over-The-Air (OTA) son un mecanismo que te permite actualizar el código JavaScript de tu aplicación React Native sin pasar por las tiendas de aplicaciones. Piensa en ello como actualizar el "cerebro" de tu app mientras mantienes el "cuerpo" (código nativo) intacto.

Cuando trabajas con React Native, es importante entender que tu aplicación está compuesta por dos partes: el código nativo (Java/Kotlin para Android, Objective-C/Swift para iOS) y el código JavaScript (tu lógica de negocio, componentes React, etc.). Las actualizaciones OTA solo pueden modificar la parte JavaScript, lo cual representa la mayor parte de tu aplicación.

¿Por qué son importantes?

Las actualizaciones OTA te dan la capacidad de corregir bugs en minutos en lugar de días. Tus usuarios obtienen la actualización automáticamente sin tener que hacer nada, lo que significa que puedes iterar rápidamente, probar features nuevas, obtener feedback inmediato e incluso hacer rollback si algo sale mal. Esta agilidad es fundamental en el desarrollo móvil moderno.

Las limitaciones que debes conocer

Antes de implementar OTA updates, necesitas entender sus restricciones. No puedes actualizar el código nativo a través de OTA, lo que significa que si cambias dependencias que modifican el código iOS o Android, necesitarás una actualización tradicional por las tiendas. Tampoco puedes cambiar permisos nativos (cámara, ubicación, etc.) ni modificar la configuración del proyecto nativo.

Esta restricción existe por razones técnicas y de seguridad. El código nativo es compilado y firmado durante el proceso de publicación en las tiendas, y las políticas de Apple y Google no permiten que este código cambie sin su revisión. Sin embargo, la mayoría de los cambios que haces día a día son en JavaScript, por lo que OTA cubre el 80-90% de tus necesidades de actualización.

Soluciones OTA disponibles para React Native CLI

En el ecosistema React Native existen principalmente dos soluciones para implementar actualizaciones OTA: CodePush de Microsoft y EAS Update de Expo. Aunque tradicionalmente CodePush era la opción predeterminada para proyectos CLI, EAS Update ahora también soporta proyectos bare React Native (CLI), ofreciendo una alternativa moderna y bien mantenida.

CodePush: La opción tradicional

CodePush fue desarrollado por Microsoft y ha sido la solución OTA más popular para React Native CLI durante años. Te permite publicar actualizaciones directamente a grupos específicos de usuarios, hacer staged rollouts y revertir actualizaciones problemáticas instantáneamente.

La ventaja principal de CodePush es su madurez y documentación extensa. Muchos equipos lo han usado en producción durante años con éxito. Sin embargo, es importante saber que Microsoft ha reducido el desarrollo activo de CodePush, lo que significa que nuevas features son menos frecuentes.

EAS Update: La alternativa moderna

EAS Update, desarrollado por Expo, es la solución más moderna y activamente mantenida. Aunque Expo tradicionalmente se asociaba con el workflow managed, ahora EAS Update funciona perfectamente con proyectos React Native CLI (bare workflow). Ofrece integración nativa con el ecosistema de Expo, mejor debugging, y updates más rápidas gracias a su infraestructura optimizada.

Si estás comenzando un proyecto nuevo, EAS Update es generalmente la mejor opción. Si ya tienes CodePush implementado y funciona bien, migrar no es urgente, pero considera EAS Update para proyectos futuros.

Implementando OTA con CodePush

Vamos a implementar OTA updates usando CodePush porque es importante que entiendas cómo funciona esta tecnología desde su origen. Una vez comprendas los conceptos aquí, migrar a EAS Update será sencillo si lo deseas.

Preparando tu entorno

Primero necesitas instalar la CLI de App Center, que es la herramienta que gestiona CodePush. App Center es la plataforma de Microsoft que aloja y distribuye tus updates. Instálala globalmente usando npm:

1npm install -g appcenter-cli

Una vez instalada, necesitas autenticarte con tu cuenta de Microsoft. Ejecuta el comando de login y sigue el proceso en el navegador:

1appcenter login

Este comando abrirá tu navegador donde podrás iniciar sesión con tu cuenta Microsoft (o crear una si no la tienes). Una vez autenticado, recibirás un token que se guardará localmente en tu máquina.

Registrando tu aplicación en App Center

Antes de poder enviar updates, necesitas registrar tu aplicación en App Center. Esto se hace separadamente para iOS y Android porque técnicamente son dos aplicaciones distintas. Crea las aplicaciones usando estos comandos:

1appcenter apps create -d MiApp-iOS -o iOS -p React-Native 2appcenter apps create -d MiApp-Android -o Android -p React-Native

Aquí -d especifica el display name (el nombre que verás en la interfaz), -o es el sistema operativo, y -p indica que es un proyecto React Native. Nota que puedes cambiar "MiApp" por el nombre de tu aplicación real.

Ahora necesitas crear los deployment keys, que son las claves que tu aplicación usará para identificarse con el servicio CodePush. Típicamente trabajas con dos ambientes: Staging (para testing) y Production (para usuarios finales):

1appcenter codepush deployment add -a <usuario>/MiApp-iOS Staging 2appcenter codepush deployment add -a <usuario>/MiApp-iOS Production 3appcenter codepush deployment add -a <usuario>/MiApp-Android Staging 4appcenter codepush deployment add -a <usuario>/MiApp-Android Production

Reemplaza <usuario> con tu nombre de usuario de App Center. Estos comandos generarán deployment keys que necesitarás en el siguiente paso.

Instalando y configurando el SDK

Ahora instala el paquete de CodePush en tu proyecto React Native:

1npm install --save react-native-code-push

Para React Native 0.60 y superior, el linking es automático gracias al autolinking, pero aún necesitas configuración adicional en el código nativo.

En iOS, abre tu archivo ios/MiApp/AppDelegate.mm y modifícalo para importar CodePush y configurar el bundle URL. Busca la línea donde se define sourceURLForBridge y reemplázala con:

1#import <CodePush/CodePush.h> 2 3- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 4{ 5 #if DEBUG 6 return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 7 #else 8 return [CodePush bundleURL]; 9 #endif 10}

Este código le dice a tu app que en modo debug use el metro bundler (para hot reloading) y en modo release use CodePush para obtener el bundle.

En Android, abre android/app/src/main/java/com/<tuapp>/MainApplication.java y agrega CodePush como un React Package. Modifica el método getJSBundleFile():

1import com.microsoft.codepush.react.CodePush; 2 3@Override 4protected String getJSBundleFile() { 5 return CodePush.getJSBundleFile(); 6}

Ahora necesitas agregar tus deployment keys a la configuración. En iOS, abre ios/MiApp/Info.plist y agrega:

1<key>CodePushDeploymentKey</key> 2<string>TU_IOS_DEPLOYMENT_KEY</string>

En Android, abre android/app/src/main/res/values/strings.xml y agrega:

1<string name="CodePushDeploymentKey">TU_ANDROID_DEPLOYMENT_KEY</string>

Puedes obtener tus deployment keys ejecutando:

1appcenter codepush deployment list -a <usuario>/MiApp-iOS --displayKeys 2appcenter codepush deployment list -a <usuario>/MiApp-Android --displayKeys

Integrando CodePush en tu código React

Ahora que la configuración nativa está lista, necesitas integrar CodePush en tu código JavaScript. La forma más simple es envolver tu componente principal con el HOC (Higher Order Component) de CodePush.

En tu App.js o App.tsx:

1import React from 'react'; 2import { Text, View } from 'react-native'; 3import codePush from 'react-native-code-push'; 4 5function App() { 6 return ( 7 <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> 8 <Text>Mi Aplicación con CodePush</Text> 9 </View> 10 ); 11} 12 13// Configuración básica de CodePush 14const codePushOptions = { 15 checkFrequency: codePush.CheckFrequency.ON_APP_RESUME, 16}; 17 18export default codePush(codePushOptions)(App);

Esta configuración básica verificará updates cada vez que el usuario regrese a la app. Existen tres estrategias principales: ON_APP_START (al iniciar), ON_APP_RESUME (al reanudar), y MANUAL (cuando tú decidas).

Publicando tu primera actualización

Ahora viene la parte emocionante: publicar tu primera actualización OTA. Haz un cambio en tu código, por ejemplo, modifica el texto en tu App.js. Luego ejecuta:

1appcenter codepush release-react -a <usuario>/MiApp-iOS -d Staging 2appcenter codepush release-react -a <usuario>/MiApp-Android -d Staging

Este comando hace varias cosas automáticamente: genera el bundle de producción de tu código JavaScript, lo empaqueta junto con los assets, lo sube a los servidores de App Center, y lo marca como disponible para el deployment especificado (Staging en este caso).

El flag -d especifica el deployment target. Primero prueba siempre en Staging, y cuando estés seguro, puedes publicar a Production:

1appcenter codepush release-react -a <usuario>/MiApp-iOS -d Production 2appcenter codepush release-react -a <usuario>/MiApp-Android -d Production

Manejando updates de forma avanzada

La configuración básica funciona, pero para una experiencia de usuario óptima querrás más control. CodePush te permite personalizar cómo y cuándo se descargan e instalan las actualizaciones.

Puedes crear una pantalla de loading mientras se descarga el update:

1import React, { useEffect, useState } from 'react'; 2import { View, Text, ActivityIndicator } from 'react-native'; 3import codePush from 'react-native-code-push'; 4 5function App() { 6 const [updateProgress, setUpdateProgress] = useState(null); 7 8 useEffect(() => { 9 codePush.sync( 10 { 11 installMode: codePush.InstallMode.IMMEDIATE, 12 updateDialog: { 13 title: "Actualización disponible", 14 optionalUpdateMessage: "Hay una nueva versión disponible. ¿Deseas actualizar?", 15 optionalInstallButtonLabel: "Sí", 16 optionalIgnoreButtonLabel: "Después", 17 }, 18 }, 19 (status) => { 20 switch (status) { 21 case codePush.SyncStatus.DOWNLOADING_PACKAGE: 22 // Update descargándose 23 break; 24 case codePush.SyncStatus.INSTALLING_UPDATE: 25 // Update instalándose 26 break; 27 } 28 }, 29 ({ receivedBytes, totalBytes }) => { 30 setUpdateProgress({ receivedBytes, totalBytes }); 31 } 32 ); 33 }, []); 34 35 if (updateProgress) { 36 const percent = (updateProgress.receivedBytes / updateProgress.totalBytes) * 100; 37 return ( 38 <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> 39 <ActivityIndicator size="large" /> 40 <Text>Descargando actualización: {percent.toFixed(0)}%</Text> 41 </View> 42 ); 43 } 44 45 return ( 46 <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> 47 <Text>Mi Aplicación</Text> 48 </View> 49 ); 50} 51 52export default App;

Esta implementación muestra un progreso visual del update y le da control al usuario sobre cuándo instalar actualizaciones opcionales.

Estrategias de rollout progresivo

Una práctica recomendada es no lanzar updates al 100% de tus usuarios inmediatamente. CodePush te permite hacer rollouts progresivos. Por ejemplo, puedes lanzar primero al 25% de usuarios:

1appcenter codepush release-react -a <usuario>/MiApp-iOS -d Production --rollout 25

Si todo va bien después de monitorear, puedes aumentar el porcentaje:

1appcenter codepush patch -a <usuario>/MiApp-iOS Production --rollout 100

Esta estrategia te da tiempo para detectar problemas antes de que afecten a todos tus usuarios.

Hardening en React Native

Ahora que entiendes cómo funcionan las actualizaciones OTA, necesitas asegurarte de que tu aplicación sea segura. Aquí es donde entra el concepto de hardening, o endurecimiento de seguridad. Hardening es el proceso de asegurar tu aplicación contra amenazas y vulnerabilidades. Cuando publicas una app móvil, estás distribuyendo tu código a millones de dispositivos que no controlas. Usuarios malintencionados pueden intentar hacer ingeniería inversa de tu app, modificar su comportamiento, o extraer información sensible.

El hardening no se trata de hacer tu app "100% imposible de hackear" (eso no existe), sino de aumentar significativamente el costo y esfuerzo necesario para comprometerla. Piénsalo como las cerraduras de tu casa: no son impenetrables, pero disuaden a la mayoría de intrusos.

¿Por qué es importante en el contexto de OTA?

Las actualizaciones OTA introducen un vector de ataque adicional: si alguien intercepta o manipula tus updates, podría inyectar código malicioso en las aplicaciones de tus usuarios. Por eso, las soluciones OTA como CodePush y EAS Update implementan verificación criptográfica de updates, pero tu responsabilidad no termina ahí.

Necesitas proteger las API keys, secrets, y datos sensibles que tu app maneja. Necesitas detectar cuando tu app está corriendo en un entorno comprometido (dispositivo rooteado/jailbroken). Y necesitas ofuscar tu código para dificultar la ingeniería inversa.

Técnicas fundamentales de hardening

Protegiendo secrets y API keys

El error más común que veo en aplicaciones React Native es guardar API keys directamente en el código JavaScript. Esto es problemático porque cualquiera puede descompilar tu bundle JavaScript y leer esas keys en texto plano.

La solución es nunca guardar secrets en el código JavaScript. En su lugar, usa variables de entorno durante el build y almacena valores sensibles en el código nativo. Aquí es donde react-native-config te ayuda.

Primero, instala la librería:

1npm install react-native-config

Crea un archivo .env en la raíz de tu proyecto (y agrégalo a .gitignore):

1API_KEY=tu_api_key_secreta 2API_ENDPOINT=https://example.com

Ahora puedes acceder a estas variables en tu código JavaScript de forma segura:

1import Config from 'react-native-config'; 2 3const apiKey = Config.API_KEY; 4const endpoint = Config.API_ENDPOINT;

Lo importante aquí es que estas variables se inyectan durante el tiempo de compilación, no en runtime. Esto significa que no están en el bundle JavaScript que se distribuye con tu app, sino en el código nativo compilado, que es significativamente más difícil de extraer.

Detectando entornos comprometidos

Los dispositivos rooteados (Android) o jailbroken (iOS) representan un riesgo porque permiten acceso privilegiado al sistema operativo. En estos entornos, un atacante puede más fácilmente interceptar el tráfico de red, modificar el comportamiento de tu app, o extraer datos almacenados.

Usa jail-monkey para detectar estos entornos:

1npm install jail-monkey

Implementa verificaciones al inicio de tu app:

1import JailMonkey from 'jail-monkey'; 2 3function AppSecurityCheck() { 4 useEffect(() => { 5 if (JailMonkey.isJailBroken()) { 6 Alert.alert( 7 'Dispositivo no seguro', 8 'Esta aplicación no puede ejecutarse en dispositivos modificados por seguridad.', 9 [{ text: 'Entendido', onPress: () => BackHandler.exitApp() }] 10 ); 11 } 12 }, []); 13 14 // resto de tu app 15}

Esta verificación no es infalible (puede ser bypasseada), pero añade una capa de protección y disuade ataques casuales. En aplicaciones que manejan datos muy sensibles (fintech, salud), podrías también verificar si hay herramientas de debugging o hooking instaladas:

1if (JailMonkey.hookDetected()) { 2 // La app está siendo modificada en runtime 3} 4 5if (JailMonkey.canMockLocation()) { 6 // El GPS puede ser falsificado 7}

Ofuscando tu código JavaScript

Aunque tu bundle JavaScript está minimizado en producción, sigue siendo relativamente fácil de leer para alguien determinado. La ofuscación hace que tu código sea mucho más difícil de entender sin afectar su funcionalidad.

Metro, el bundler de React Native, no ofrece ofuscación por defecto. Necesitas usar metro-transform-plugins o herramientas como javascript-obfuscator. Aquí te muestro cómo integrar ofuscación en tu proceso de build.

Instala el obfuscador:

1npm install --save-dev javascript-obfuscator metro-transform-plugins

Crea un archivo metro.config.js si no lo tienes, y configura el transformer:

1const { getDefaultConfig } = require('@react-native/metro-config'); 2const JavaScriptObfuscator = require('javascript-obfuscator'); 3 4module.exports = (async () => { 5 const defaultConfig = await getDefaultConfig(__dirname); 6 7 return { 8 ...defaultConfig, 9 transformer: { 10 ...defaultConfig.transformer, 11 minifierPath: 'metro-minify-terser', 12 minifierConfig: { 13 // Configuración para producción 14 compress: { 15 drop_console: true, // Elimina console.logs 16 }, 17 }, 18 }, 19 }; 20})();

Para una ofuscación más agresiva, puedes crear un plugin personalizado que procese tu bundle después de la compilación:

1// obfuscator-plugin.js 2const JavaScriptObfuscator = require('javascript-obfuscator'); 3const fs = require('fs'); 4 5function obfuscateBundle(bundlePath) { 6 const code = fs.readFileSync(bundlePath, 'utf8'); 7 8 const obfuscated = JavaScriptObfuscator.obfuscate(code, { 9 compact: true, 10 controlFlowFlattening: true, 11 controlFlowFlatteningThreshold: 0.75, 12 deadCodeInjection: true, 13 deadCodeInjectionThreshold: 0.4, 14 debugProtection: false, // true en producción puede afectar performance 15 debugProtectionInterval: 0, 16 disableConsoleOutput: true, 17 identifierNamesGenerator: 'hexadecimal', 18 log: false, 19 renameGlobals: false, 20 rotateStringArray: true, 21 selfDefending: true, 22 stringArray: true, 23 stringArrayEncoding: ['base64'], 24 stringArrayThreshold: 0.75, 25 unicodeEscapeSequence: false, 26 }); 27 28 fs.writeFileSync(bundlePath, obfuscated.getObfuscatedCode()); 29} 30 31module.exports = { obfuscateBundle };

Ten en cuenta que la ofuscación agresiva puede aumentar el tamaño del bundle y afectar ligeramente el performance. Encuentra el balance correcto para tu aplicación haciendo pruebas.

Asegurando la comunicación de red

Todas las comunicaciones entre tu app y tus servidores deben estar encriptadas usando HTTPS/TLS. Pero eso no es suficiente: también necesitas implementar SSL Pinning para prevenir ataques man-in-the-middle.

SSL Pinning significa que tu app solo acepta certificados específicos que tú defines, incluso si el dispositivo tiene certificados falsos instalados. Usa react-native-ssl-pinning para implementarlo:

1npm install react-native-ssl-pinning

Configura el pinning en tu código:

1import { fetch } from 'react-native-ssl-pinning'; 2 3const response = await fetch('https://example.com/data', { 4 method: 'GET', 5 timeoutInterval: 10000, 6 pkPinning: true, 7 sslPinning: { 8 certs: ['mi-certificado'], // nombre del archivo .cer en tu bundle 9 }, 10 headers: { 11 'Accept': 'application/json', 12 'Content-Type': 'application/json', 13 }, 14});

Coloca tu archivo de certificado (.cer) en:

  • iOS: ios/MiApp/mi-certificado.cer
  • Android: android/app/src/main/assets/mi-certificado.cer

Esta técnica previene que alguien intercepte tu tráfico instalando un certificado falso en el dispositivo, algo común en entornos corporativos o en dispositivos comprometidos.

Almacenamiento seguro de datos

Nunca uses AsyncStorage para datos sensibles. AsyncStorage no está encriptado y puede leerse fácilmente en dispositivos rooteados o mediante backup. Para datos sensibles usa el keychain nativo.

Instala react-native-keychain:

1npm install react-native-keychain

Guarda datos sensibles de forma segura:

1import * as Keychain from 'react-native-keychain'; 2 3// Guardar 4await Keychain.setGenericPassword('username', 'password', { 5 service: 'com.miapp', 6 accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY, 7 accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED, 8}); 9 10// Recuperar 11const credentials = await Keychain.getGenericPassword({ 12 service: 'com.miapp', 13}); 14 15if (credentials) { 16 console.log('Usuario:', credentials.username); 17 console.log('Password:', credentials.password); 18}

Los datos guardados con Keychain usan el Secure Enclave del dispositivo y están protegidos con encriptación de hardware. Además, puedes requerir autenticación biométrica para acceder a ellos.

Integrando hardening con OTA updates

Ahora que entiendes tanto OTA como hardening por separado, hablemos de cómo trabajan juntos y las consideraciones especiales que necesitas tener.

Verificación de integridad de updates

CodePush y EAS Update implementan verificación criptográfica de updates por defecto. Cada update es firmado con claves privadas en los servidores y tu app verifica la firma antes de instalar el update. Esto previene que alguien inyecte código malicioso.

Sin embargo, debes asegurarte de que tus deployment keys estén protegidas. Nunca las commites a tu repositorio Git y usa variables de entorno para manejarlas:

1# .env.production 2CODEPUSH_KEY_IOS=tu_key_ios 3CODEPUSH_KEY_ANDROID=tu_key_android

En tu pipeline de CI/CD, inyecta estas variables como secretos encriptados. Si usas GitHub Actions:

1- name: Release CodePush Update 2 env: 3 CODEPUSH_KEY: ${{ secrets.CODEPUSH_KEY_IOS }} 4 run: | 5 appcenter codepush release-react \ 6 -a usuario/MiApp-iOS \ 7 -d Production \ 8 --deployment-key $CODEPUSH_KEY

Rollback seguro

Una ventaja importante de OTA es la capacidad de hacer rollback instantáneo si detectas un problema. Puedes revertir a una versión anterior en segundos:

1appcenter codepush rollback -a <usuario>/MiApp-iOS Production

Implementa monitoreo activo de errores (usando Sentry o similar) para detectar problemas rápidamente después de un release:

1import * as Sentry from '@sentry/react-native'; 2import codePush from 'react-native-code-push'; 3 4// Reporta la versión de CodePush a Sentry 5codePush.getUpdateMetadata().then((metadata) => { 6 if (metadata) { 7 Sentry.setTag('codepush.version', metadata.label); 8 Sentry.setTag('codepush.deployment', metadata.deploymentKey); 9 } 10});

Esto te permite correlacionar errores con versiones específicas de tu bundle y hacer rollback informado.

Actualizaciones forzadas vs opcionales

Para bugs de seguridad críticos, querrás forzar la actualización. CodePush soporta updates mandatorias:

1appcenter codepush release-react \ 2 -a <usuario>/MiApp-iOS \ 3 -d Production \ 4 --mandatory true

En tu código, las updates mandatorias se instalan automáticamente:

1codePush.sync({ 2 installMode: codePush.InstallMode.IMMEDIATE, 3 mandatoryInstallMode: codePush.InstallMode.IMMEDIATE, 4});

Usa esta opción con cuidado. Updates mandatorias inmediatas reinician la app automáticamente, lo cual puede frustrar a usuarios en medio de una tarea. Para la mayoría de updates, es mejor usar ON_NEXT_RESTART.

Mejores prácticas para un sistema OTA+Hardening robusto

Implementa versionado semántico estricto

Mantén un esquema de versionado claro que diferencie entre versiones de la app store y versions OTA. Una convención común es:

[appstore_version].[codepush_version]

Ejemplo: 1.2.0.5
- 1.2.0 es la versión en la App Store
- 5 es la quinta actualización OTA sobre esa versión base

Guarda esta información en tu app para debugging:

1import { getVersion, getBuildNumber } from 'react-native-device-info'; 2import codePush from 'react-native-code-push'; 3 4export async function getAppVersion() { 5 const cpMetadata = await codePush.getUpdateMetadata(); 6 const storeVersion = getVersion(); // 1.2.0 7 const buildNumber = getBuildNumber(); // 42 8 const cpLabel = cpMetadata?.label || 'none'; // v5 9 10 return { 11 full: `${storeVersion}.${cpLabel}`, 12 store: storeVersion, 13 build: buildNumber, 14 codePush: cpLabel, 15 }; 16}

Automatiza tu pipeline de release

Nunca hagas releases manuales. Automatiza todo el proceso para evitar errores humanos y asegurar consistencia. Un ejemplo con GitHub Actions:

1name: Release OTA Update 2 3on: 4 push: 5 branches: 6 - main 7 8jobs: 9 release-codepush: 10 runs-on: ubuntu-latest 11 steps: 12 - uses: actions/checkout@v2 13 14 - name: Setup Node 15 uses: actions/setup-node@v2 16 with: 17 node-version: '18' 18 19 - name: Install dependencies 20 run: npm ci 21 22 - name: Run tests 23 run: npm test 24 25 - name: Install AppCenter CLI 26 run: npm install -g appcenter-cli 27 28 - name: Login to AppCenter 29 run: appcenter login --token ${{ secrets.APPCENTER_TOKEN }} 30 31 - name: Release to CodePush Staging 32 run: | 33 appcenter codepush release-react \ 34 -a ${{ secrets.APPCENTER_APP_IOS }} \ 35 -d Staging \ 36 --description "Auto-release: ${{ github.event.head_commit.message }}"

Este workflow ejecuta tests, luego automáticamente publica a Staging cada vez que haces push a main. Puedes extenderlo con steps adicionales para publicar a Production después de validación.

Implementa feature flags

Los feature flags te permiten activar o desactivar funcionalidades remotamente sin hacer un nuevo release. Esto es especialmente poderoso combinado con OTA. Puedes desplegar código nuevo pero mantenerlo desactivado, probarlo internamente, y activarlo gradualmente.

Usa una librería como react-native-config-reader o servicios como LaunchDarkly:

1import { getFeatureFlag } from './featureFlags'; 2 3function MyFeature() { 4 const isEnabled = getFeatureFlag('new_feature'); 5 6 if (!isEnabled) { 7 return <LegacyFeature />; 8 } 9 10 return <NewFeature />; 11}

Esto te da un "kill switch" instantáneo: si una nueva feature causa problemas, la desactivas sin hacer rollback del código completo.

Monitoreo y observabilidad

Implementa telemetría comprehensiva para entender cómo se comportan tus updates en el campo. Además de crash reporting, monitorea:

1// Trackear adopción de updates 2codePush.getUpdateMetadata().then((metadata) => { 3 if (metadata) { 4 analytics.track('CodePush Update Installed', { 5 label: metadata.label, 6 appVersion: metadata.appVersion, 7 deploymentKey: metadata.deploymentKey, 8 description: metadata.description, 9 isFirstRun: metadata.isFirstRun, 10 }); 11 } 12}); 13 14// Trackear tiempo de descarga 15const startTime = Date.now(); 16codePush.sync( 17 { ... }, 18 (status) => { 19 if (status === codePush.SyncStatus.UPDATE_INSTALLED) { 20 const downloadTime = Date.now() - startTime; 21 analytics.track('Update Download Time', { 22 milliseconds: downloadTime, 23 }); 24 } 25 } 26);

Esta información te ayuda a optimizar el tamaño de tus bundles y entender patrones de adopción.

Checklist de seguridad pre-producción

Antes de lanzar tu app con OTA habilitado, verifica que cumples estos requisitos de seguridad:

Protección de secrets:

  • Ninguna API key o secret en el código JavaScript
  • Variables de entorno para configuración sensible
  • Deployment keys de CodePush/EAS Update en CI/CD secrets

Detección de compromisos:

  • Verificación de jailbreak/root al inicio
  • Detección de debugging tools en producción
  • Alertas cuando la app corre en entorno no seguro

Ofuscación y protección de código:

  • Bundle JavaScript ofuscado en producción
  • Console.logs removidos del bundle final
  • Source maps protegidos (no públicos)

Comunicación segura:

  • Todo el tráfico sobre HTTPS/TLS
  • SSL Pinning implementado para endpoints críticos
  • Timeout y retry logic para requests de red

Almacenamiento:

  • Datos sensibles en Keychain nativo
  • AsyncStorage solo para datos no sensibles
  • Encriptación adicional para PII si es necesario

OTA específico:

  • Staged rollouts configurados (25% → 50% → 100%)
  • Monitoreo activo de errores post-release
  • Plan de rollback documentado y probado
  • Updates mandatorias solo para seguridad crítica

En conclusion las actualizaciones OTA te dan una agilidad increíble para iterar y corregir problemas rápidamente. El hardening asegura que esta agilidad no comprometa la seguridad de tus usuarios. Estas dos prácticas no están en conflicto, sino que se complementan.

Recuerda que la seguridad es un proceso continuo, no un estado final. Mantente actualizado sobre nuevas vulnerabilidades, actualiza tus dependencias regularmente, y revisa tu implementación de seguridad con cada release mayor.