react native
ci-cd
mobile-security
eas-update
devops
codepush
ota-updates
hardening
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.
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.
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.
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.
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 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, 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.
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.
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
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.
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
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).
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
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.
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.
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.
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.
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.
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}
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.
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/MiApp/mi-certificado.cerandroid/app/src/main/assets/mi-certificado.cerEsta 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.
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.
Ahora que entiendes tanto OTA como hardening por separado, hablemos de cómo trabajan juntos y las consideraciones especiales que necesitas tener.
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
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.
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.
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}
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.
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.
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.
Antes de lanzar tu app con OTA habilitado, verifica que cumples estos requisitos de seguridad:
Protección de secrets:
Detección de compromisos:
Ofuscación y protección de código:
Comunicación segura:
Almacenamiento:
OTA específico:
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.