App Móvil con React Native + AWS Cognito
Aplicación móvil multiplataforma iOS/Android con autenticación Cognito, API serverless y sincronización offline.
Problema & Solución
Problema
Construir una app móvil que funcione en iOS y Android con autenticación segura, acceso a una API serverless en AWS, y capacidad de funcionar offline con sincronización automática cuando se restaura la conexión.
Solución
React Native + Expo para desarrollo multiplataforma con TypeScript. AWS Amplify SDK para integración con Cognito (User Pools + Identity Pools), API Gateway y AppSync. Cognito maneja el flujo completo de auth: registro, confirmación por email, login, MFA opcional, refresh automático de tokens. Las llamadas a la API van firmadas con credenciales temporales STS (Cognito Identity Pool). SQLite local (via expo-sqlite) para cache offline con sincronización delta.
Diagrama de Arquitectura
┌──────────────────────────────────────────────────────────────┐
│ React Native App (Expo) │
│ │
│ ┌─────────────────────────┐ ┌──────────────────────────┐ │
│ │ Auth Flow │ │ Data Layer │ │
│ │ • Sign Up │ │ • React Query (cache) │ │
│ │ • Email Confirm │ │ • SQLite (offline) │ │
│ │ • Sign In │ │ • Sync Queue │ │
│ │ • MFA (TOTP) │ └──────────────────────────┘ │
│ │ • Token Refresh (auto) │ │
│ └─────────────────────────┘ │
└─────────────────────────┬────────────────────────────────────┘
│ HTTPS (Amplify SDK)
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌────────────────┐ ┌─────────────────┐
│ Cognito │ │ API Gateway │ │ AppSync │
│ User Pool │ │ (REST) │ │ (GraphQL/WS) │
│ │ │ + Lambda │ │ + DynamoDB │
│ Identity │ │ (business │ │ (real-time │
│ Pool │ │ logic) │ │ subscriptions)│
└─────────────┘ └────────────────┘ └─────────────────┘
│
▼ (credenciales temporales STS)
IAM Role → permisos para llamar API Gateway y AppSyncImplementación
Configuración de Cognito User Pool + Identity Pool
User Pool define las reglas de contraseña, atributos del usuario (email, nombre), políticas de MFA, y triggers Lambda (pre-signup, post-confirmation). Identity Pool mapea usuarios autenticados de Cognito a un IAM Role con permisos temporales STS. Los tokens JWT de Cognito se intercambian por credenciales AWS temporales (AccessKeyId + SecretAccessKey + SessionToken).
Flujo de autenticación con Amplify SDK
Amplify maneja automáticamente el refresh de tokens: cuando el accessToken expira (1 hora), usa el refreshToken para obtener uno nuevo sin que el usuario lo note. El token se almacena en SecureStore (iOS Keychain / Android Keystore). Si el refreshToken expira (30 días por defecto), el usuario debe volver a hacer login.
Llamadas firmadas a API Gateway con IAM Auth
Con las credenciales STS del Identity Pool, el SDK de Amplify firma automáticamente cada request a API Gateway con SigV4. API Gateway valida la firma y el IAM Role. Este approach es más seguro que API Keys porque las credenciales son temporales (1 hora) y tienen scope limitado al rol del Identity Pool.
Offline-first con SQLite y sync queue
expo-sqlite almacena datos localmente. Cuando hay conexión, se consulta la API y se actualiza SQLite. Cuando no hay conexión, la app lee de SQLite directamente. Las operaciones de escritura offline se añaden a una sync queue. Al recuperar conectividad (NetInfo listener), se procesan en orden con manejo de conflictos (last-write-wins o server-wins según el tipo de dato).
Tech Stack
Mobile Framework
UI & Navigation
AWS & Auth
Data & Offline
Decisiones Técnicas
Expo vs React Native CLI puro
Elegido
Expo (Managed Workflow)
Alternativas
- —React Native CLI — control total, requiere configurar Xcode/Android Studio
- —React Native CLI + Expo modules — lo mejor de ambos mundos, más complejo
Razón
Expo managed workflow elimina la necesidad de Xcode y Android Studio para el desarrollo del día a día. EAS Build compila en la nube. Para un portfolio, la productividad importa más que el control del código nativo. Si se necesitan módulos nativos no disponibles en Expo, se puede hacer eject o usar Expo bare workflow.
Snippets de Código
// amplify-config.ts
import { Amplify } from "aws-amplify";
Amplify.configure({
Auth: {
Cognito: {
userPoolId: process.env.EXPO_PUBLIC_USER_POOL_ID!,
userPoolClientId: process.env.EXPO_PUBLIC_CLIENT_ID!,
identityPoolId: process.env.EXPO_PUBLIC_IDENTITY_POOL_ID!,
loginWith: {
email: true,
},
mfa: {
status: "optional",
totpEnabled: true,
},
passwordFormat: {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireNumbers: true,
requireSpecialCharacters: true,
},
},
},
});
// hooks/useAuth.ts
import { useState, useEffect } from "react";
import {
signIn,
signUp,
signOut,
getCurrentUser,
fetchAuthSession,
AuthUser,
} from "aws-amplify/auth";
export function useAuth() {
const [user, setUser] = useState<AuthUser | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
getCurrentUser()
.then(setUser)
.catch(() => setUser(null))
.finally(() => setLoading(false));
}, []);
const login = async (email: string, password: string) => {
const result = await signIn({ username: email, password });
if (result.isSignedIn) {
setUser(await getCurrentUser());
}
return result;
};
const logout = async () => {
await signOut();
setUser(null);
};
const getToken = async (): Promise<string> => {
const session = await fetchAuthSession();
return session.tokens?.idToken?.toString() ?? "";
};
return { user, loading, login, logout, getToken };
}