study.plynte.com
@franguh
Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)

Curso autodirigido: Rust y full stack#

Aprende a construir y diagnosticar software web desde los fundamentos: primero Rust independiente, después contratos HTTP y persistencia, y luego navegador, TypeScript e integración. El recorrido prioriza entender mecanismos, observar evidencia y justificar cambios; no requiere un producto empresarial ni promete una duración fija.

Mapa del curso: seis tramos y un ciclo de práctica
Mapa del curso: seis tramos y un ciclo de práctica

Para una referencia terminológica exhaustiva a lo largo de todo el curso, consulta el glosario y diccionario técnico.


Ruta del curso#

MóduloLeccionesEnfoque central
00 · InicioPrerrequisitos, método de estudio y tutoría con IAConfiguración local, protocolo de falsación y herramientas mínimas.
01 · Rust01 Tipos y flujo · 02 Ownership y préstamos · 03 Errores y pruebas · 04 Colecciones e iteraciónSemántica de memoria, propiedad única, préstamos sin recolector de basura y tipos algebraicos.
02 · Backend05 Contratos HTTP · 06 Límites con Actix · 07 Invariantes PostgreSQL · 08 Async y concurrenciaLímites de serialización, enrutamiento tipado, transacciones ACID y concurrencia asíncrona.
03 · Navegador09 HTML, CSS y accesibilidad · 10 TypeScript y DOM · 11 Estado en React · 12 Integración de extremo a extremoÁrbol DOM, estrechamiento estático de tipos, reconciliación de estado y comunicación cliente-servidor.
04 · Diseño13 Code smells y refactorización · 14 Límites y dependencias · 15 Pruebas y depuraciónDiagnóstico de acoplamiento, inversión de dependencias, pirámide de pruebas y aislamiento de fallos.
05 · Operación16 Autenticación y autorización · 17 Entrega y observabilidad · 18 Diagnóstico full stackIdentidad criptográfica, métricas/trazas estructuradas, empaquetado reproducible y resolución de incidentes.

Propósito pedagógico y niveles de corrección#

Este curso adopta una postura pedagógica rigurosa: construir software robusto exige distinguir con absoluta claridad entre tres niveles de validez que a menudo se confunden:

  1. Validez sintáctica (Gramática formal): El código cumple las reglas léxicas y gramaticales del lenguaje. Puede ser analizado por el analizador sintáctico (parser) y transformado en un árbol de sintaxis abstracta (AST). Un programa sintácticamente válido puede carecer de cualquier coherencia de tipos o lógica operativa.
  2. Verificación de tipos y análisis estático (Reglas del compilador): El compilador comprueba que las operaciones invocadas sean compatibles con los tipos declarados, que las variables estén inicializadas y, en el caso de Rust, que los tiempos de vida (lifetimes) y las reglas de propiedad garanticen seguridad de memoria en tiempo de compilación. Un programa que supera este nivel no genera desbordamientos de búfer ni carreras de datos en memoria.
  3. Corrección semántica (Reglas del dominio del problema): El programa hace exactamente lo que el problema del mundo real requiere. Resuelve los casos borde, preserva los invariantes de negocio y responde con fallos controlados ante entradas inválidas.
🛡️ Información Crítica

Que un programa compile sin advertencias jamás demuestra que sea correcto. Un programa en Rust puede compilar a la perfección pero restar saldo cuando debería sumar, permitir duplicados en un conjunto conceptual, o responder un código 200 OK con un cuerpo que describe un error fatal de base de datos. El compilador es tu primera barrera matemática contra fallos de memoria y tipos; las pruebas rigurosas y el razonamiento sobre invariantes son tu defensa frente a fallos semánticos.

Datos neutrales frente a la fontanería accidental de SaaS#

A lo largo del curso se utilizan estructuras de datos pequeñas y neutrales (por ejemplo, notas con título y cuerpo, tareas con estado de completado, o listas de elementos indexados). Esta decisión de diseño curricular es deliberada:

  • Complejidad esencial frente a complejidad accidental: La complejidad esencial de la ingeniería de software radica en cómo los datos fluyen a través de límites, cómo se transforman tipos, cómo se maneja la concurrencia y cómo se garantiza la atomicidad en la persistencia. La complejidad accidental comprende configurar paneles de control en la nube, registrar tarjetas de crédito para servicios de terceros, resolver tokens OAuth de proveedores externos o batallar con SDKs comerciales cerrados.
  • Sin distracciones de infraestructura: Al evitar el "SaaS plumbing", todo el tiempo mental se dedica a observar qué ocurre en el puntero de la pila, en el búfer de la memoria dinámica, en el descriptor de archivo de la base de datos o en el ciclo de eventos del navegador.

Diccionario base antes de empezar (Glosario de arranque)#

Para estudiar con precisión, es necesario compartir un vocabulario riguroso. Cada uno de los siguientes diez términos fundamentales se define desde primeros principios mediante cuatro dimensiones: intuición cotidiana, rigor técnico, ejemplo concreto en código o sistema, y delimitación clara de lo que no es.

1. Invariante#

  • Intuición cotidiana: Una regla inquebrantable de la naturaleza en un entorno determinado. En un juego de ajedrez, el rey nunca puede ocupar una casilla amenazada al terminar un turno; no importa qué jugada ocurra, esa condición debe permanecer intacta.
  • Definición técnica precisa: Condición o predicado lógico que debe evaluarse como verdadero durante toda la vida útil de una estructura de datos o de un sistema, excepto durante mutaciones intermedias estrictamente controladas que restauran la condición antes de ceder el control.
  • Ejemplo en código:
RUST
  // Invariant: The title cannot be empty or contain only whitespace.
  #[derive(Debug, PartialEq, Eq)]
  pub struct NonEmptyTitle(String);

  impl NonEmptyTitle {
      pub fn parse(raw: String) -> Result<Self, &'static str> {
          let trimmed = raw.trim();
          if trimmed.is_empty() {
              Err("Title cannot be empty")
          } else {
              Ok(Self(trimmed.to_string()))
          }
      }

      pub fn as_str(&self) -> &str {
          &self.0
      }
  }
  • Lo que NO es: No es una mera validación en un formulario web que se comprueba una sola vez y se olvida. No es una sugerencia ni una convención en un comentario de código. Si una estructura permite que un usuario modifique un campo público violando la regla, la condición deja de ser un invariante y se convierte en una simple aspiración.

2. Modelo mental#

  • Intuición cotidiana: El mapa esquemático que un maquinista tiene en su mente sobre cómo las palancas y válvulas mueven el vapor y los pistones de una locomotora, permitiéndole anticipar qué pasará si presiona una válvula antes de tocarla.
  • Definición técnica precisa: Abstracción interna y estructurada que un ingeniero construye sobre el estado interno, el flujo de ejecución, la topología de memoria y la interacción entre componentes de un sistema computacional. Permite formular predicciones falsables sobre el comportamiento del sistema ante estímulos específicos sin depender del ensayo y error.
  • Ejemplo en el sistema: Al invocar una función en Rust que recibe un parámetro por valor (fn consume(data: String)), tu modelo mental visualiza que el descriptor de 24 bytes en la pila (stack) —puntero al heap, capacidad y longitud— se copia a la nueva trama de la función llamada, y que la función que llama pierde inmediatamente el permiso de lectura sobre esos 24 bytes.
  • Lo que NO es: No es memorizar la sintaxis de un lenguaje ni recordar de memoria las firmas de una biblioteca. Un desarrollador puede memorizar decenas de métodos de array y carecer por completo de un modelo mental sobre si una operación ejecuta una copia profunda en el heap o pasa un puntero por referencia.

3. Evidencia observable#

  • Intuición cotidiana: La marca física dejada en la nieve por un neumático, contrastada con la afirmación no comprobada de alguien que dice "creo que un coche pasó por aquí".
  • Definición técnica precisa: Información empírica, verificable, medible y reproducible obtenida a través de la salida estándar de una herramienta formal: mensajes de error del compilador con números de línea, códigos de retorno de procesos del sistema operativo, marcos de depuración (stack traces), respuestas HTTP capturadas con sus cabeceras y bytes de cuerpo, o volcados de memoria y registros estructurados.
  • Ejemplo en el sistema:
TEXT
  error[E0382]: borrow of moved value: `entry_title`
    --> src/main.rs:14:26
     |
  10 |     let entry_title = String::from("Rust Invariants");
     |         ----------- move occurs because `entry_title` has type `String`, which does not implement the `Copy` trait
  11 |     consume(entry_title);
     |             ----------- value moved here
  ...
  14 |     println!("{}", entry_title);
     |                    ^^^^^^^^^^^ value borrowed here after move
  • Lo que NO es: No es una intuición subjetiva como "creo que el código falla por los préstamos" ni la afirmación "ya probé el endpoint y parece que funciona". Sin la transcripción textual del comando ejecutado y la salida arrojada, no existe evidencia.

4. Criterio de salida#

  • Intuición cotidiana: La lista de verificación indiscutible para dar por terminada la revisión de un vehículo antes de salir a la carretera: nivel de aceite verificado con varilla, presión de neumáticos a 32 PSI y luces de freno operativas.
  • Definición técnica precisa: Conjunto unívoco, determinista y comprobable de condiciones de éxito y pruebas de falsación que deben satisfacerse por completo antes de considerar concluida una lección, una tarea o un ciclo de desarrollo.
  • Ejemplo en el sistema: "La prueba unitaria adds_two_numbers compila con rustc --test, se ejecuta retornando código de salida 0, y al modificar deliberadamente la aserción para esperar un valor falso, el arnés de pruebas falla reportando con exactitud la discrepancia observada entre left y right."
  • Lo que NO es: No es "haber leído el documento hasta el final", ni "haber visto el video explicativo", ni haber copiado y pegado un fragmento de código sin haberlo sometido a un experimento adverso.

5. Contrato (de función o HTTP)#

  • Intuición cotidiana: Un acuerdo legal firmado: el proveedor se compromete a entregar una mercancía con especificaciones exactas siempre y cuando el cliente pague en la moneda y plazos acordados. Si el cliente paga de menos, el proveedor tiene el derecho explícito de rechazar la orden.
  • Definición técnica precisa: Especificación formal y vinculante de precondiciones, postcondiciones e invariantes entre dos partes que interactúan (ya sean dos módulos de software en el mismo proceso o dos nodos a través de una red). Define qué garantiza el receptor dado un conjunto conforme de datos de entrada, y cómo se comunican y tipifican los rechazos ante entradas no conformes.
  • Ejemplo en código y HTTP:
  • Precondición de función: Debe recibir un entero u32 estrictamente mayor que cero.
  • Postcondición: Retorna la raíz cuadrada exacta o un error tipado de tipo MathError::NotPerfectSquare.
  • Contrato HTTP: POST /tasks exige cabecera Content-Type: application/json y un cuerpo JSON con campo title de 1 a 100 caracteres. Garantiza status 201 Created con cabecera Location y el recurso creado, o 400 Bad Request con un esquema de error estándar si el JSON está malformado.
  • Lo que NO es: No es una sugerencia opcional en un archivo de texto desactualizado. Si el backend acepta silenciosamente datos corruptos que violan su especificación documentada, el contrato está roto.

6. Persistencia#

  • Intuición cotidiana: Escribir un registro con tinta indeleble en un libro de actas físico guardado en una caja fuerte, en lugar de anotarlo en una pizarra de tiza que se borra al desconectar la luz de la habitación.
  • Definición técnica precisa: Capacidad de un estado de datos de sobrevivir al ciclo de vida del proceso de cómputo que lo originó, resistiendo apagados programados, reinicios forzados y fallos del sistema operativo, mediante su almacenamiento en medios no volátiles (discos de estado sólido, discos magnéticos o memoria no volátil) bajo protocolos que garantizan durabilidad (como el vaciado de búferes con fsync y el registro previo de transacciones Write-Ahead Logging).
  • Ejemplo en el sistema: Insertar una fila en una base de datos relacional y ejecutar un COMMIT. El motor asegura que los bytes correspondientes se han escrito en el registro de transacciones en disco antes de devolver el control a la aplicación cliente.
  • Lo que NO es: No es mantener una variable global o una lista en memoria (Vec<T> o un objeto de JavaScript en el servidor) que desaparece instantáneamente en cuanto el proceso se detiene o el servidor se reinicia.

7. Límite arquitectónico (Boundary)#

  • Intuición cotidiana: La garita de aduanas en una frontera internacional: todo viajero y paquete que llega de territorio foráneo es inspeccionado, sus pasaportes son verificados y se decomisan mercancías prohibidas antes de permitirles el ingreso al territorio nacional.
  • Definición técnica precisa: Punto de contacto, transición y traducción entre dos dominios, niveles de confianza o subsistemas tecnológicos dispares (por ejemplo, red no confiable y dominio de negocio, proceso de aplicación y motor de base de datos, o código de servidor y navegador). En el límite, los datos no estructurados o poco tipados (texto en crudo, bytes TCP, JSON genérico) deben validarse, parsearse y transformarse en tipos de dominio estrictos con garantías de invariantes.
  • Ejemplo en el sistema: El deserializador de Actix Web o Serde que recibe una secuencia de bytes UTF-8 desde una solicitud HTTP entrante y la transforma en una estructura fuertemente tipada CreateTaskRequest, devolviendo de inmediato un error 422 Unprocessable Entity si la estructura no satisface los requisitos.
  • Lo que NO es: No es una simple división de carpetas dentro del proyecto. Si el código de la base de datos o el deserializador HTTP pasa datos crudos y no validados directamente al núcleo de tu lógica de negocio, no existe un límite arquitectónico efectivo; los subsistemas están acoplados y expuestos.

8. Code smell#

  • Intuición cotidiana: Un olor agrio persistente al abrir la puerta del refrigerador. No indica que la comida haya explotado ni que el electrodoméstico esté roto hoy, pero advierte con certeza que algo se está pudriendo en el fondo y causará un problema grave si no se atiende.
  • Definición técnica precisa: Síntoma superficial y reconocible en el código fuente que, sin constituir un error de sintaxis ni impedir la compilación o ejecución, sugiere una debilidad de diseño subyacente, un acoplamiento indebido, una violación de abstracción o una acumulación de deuda técnica que incrementa el riesgo de fallos futuros o dificulta el mantenimiento.
  • Ejemplo en código: El uso sistemático de data.clone() en Rust para apaciguar al borrow checker cada vez que surge un conflicto de propiedad, o una función de 300 líneas con 8 niveles de anidamiento condicional que realiza validación, llamadas SQL y formateo HTML en un único bloque.
  • Lo que NO es: No es un error de ejecución (bug) evidente ni un fallo de compilación. El código con code smells puede pasar todas las pruebas funcionales actuales. Tampoco es una cuestión de preferencia estética subjetiva (como dónde colocar llaves o espacios); es un indicador empírico de riesgo estructural.

9. Refactorización#

  • Intuición cotidiana: Reorganizar por completo el cableado interno y los conductos detrás de las paredes de un edificio para que sean seguros, accesibles y modulares, garantizando que todos los interruptores, enchufes y grifos sigan funcionando exactamente igual para los residentes.
  • Definición técnica precisa: Técnica disciplinada para alterar la estructura interna de un sistema de software con el fin de mejorar su legibilidad, modularidad, rendimiento o extensibilidad, sin modificar en absoluto su comportamiento observable externo, respaldada por una suite de pruebas automatizadas que se ejecutan antes y después de cada cambio mínimo.
  • Ejemplo en el sistema: Extraer un algoritmo de cálculo duplicado en tres funciones hacia una función pura independiente, verificando que la totalidad de las pruebas unitarias existentes pasan en verde antes de empezar y continúan pasando en verde tras la extracción.
  • Lo que NO es: No es "reescribir el software desde cero", ni "aprovechar para agregar una nueva funcionalidad", ni "cambiar nombres de variables al azar". Si el comportamiento observable cambia (una respuesta HTTP varía su código o un cálculo arroja un resultado diferente), no es refactorización: es modificación de comportamiento o introducción de regresiones.

10. Observabilidad#

  • Intuición cotidiana: El instrumental de vuelo en la cabina de un avión (altímetro, indicador de combustible, radar de tormentas y caja negra) que permite a los pilotos saber exactamente qué ocurre en los motores y en el fuselaje en medio de una tormenta de noche, sin tener que salir al ala a mirar.
  • Definición técnica precisa: Propiedad de un sistema que permite inferir y diagnosticar su estado interno, fallos y cuellos de botella exclusivamente a partir de sus salidas externas estructuradas (métricas cuantitativas, trazas distribuidas con identificadores de correlación y registros de eventos contextuales).
  • Ejemplo en el sistema: Un servicio web que, ante cada solicitud entrante, genera un identificador único request_id, registra el tiempo exacto consumido en cada consulta a la base de datos, emite métricas de latencia al percentil 99 y formatea los registros en JSON con campos estructurados (timestamp, level, trace_id, duration_ms).
  • Lo que NO es: No es llenar el código de sentencias desordenadas println!("aquí pasó 1") que imprimen texto plano sin contexto, marcas temporales ni identificadores de correlación en la consola del servidor.

Empieza ahora#

Sigue esta secuencia para iniciar tu trabajo con disciplina:

  1. Lee Inicio, ejecuta las comprobaciones de versión en tu terminal y revisa la guía de laboratorio opcional en Windows para preparar un entorno de práctica local aislado.
  2. Consulta el glosario y diccionario técnico si encuentras cualquier término cuyo mecanismo interno no puedas justificar desde primeros principios.
  3. Abre la Lección 01: Tipos y flujo. Formula y escribe una predicción explícita sobre qué hará el compilador o la máquina antes de ejecutar cualquier comando.
  4. Registra tu evidencia observable —lo que puedes explicar, probar y modificar con resultados reales de terminal— en tu propio PROGRESS.md. No marques casillas por el mero hecho de haber leído una página.

Las prácticas del curso utilizan entidades neutrales (notas, tareas, registros de texto), concebidas exclusivamente para razonar sobre invariantes de estado, memoria y concurrencia. No se entrega una aplicación comercial, producto SaaS ni infraestructura en la nube. Las guías de respuestas se mantienen deliberadamente separadas para no sesgar tu proceso inductivo:

Para materiales visuales y fuentes complementarias, revisa el catálogo de recursos oficiales y la galería de diagramas editables y SVG.


Preguntas de autodiagnóstico inicial (para reflexionar antes de la Lección 01)#

Antes de escribir tu primera línea de código en Rust, dedica tiempo a reflexionar sobre estas cuatro preguntas conceptuales de diagnóstico. No busques respuestas rápidas en un buscador; evalúa tu propio modelo mental actual y utiliza las guías de razonamiento para identificar qué fundamentos necesitas reforzar.

Pregunta 1: Compilación exitosa frente a corrección del sistema#

Imagina que escribes una función que debe transferir fondos entre dos cuentas bancarias. El compilador de Rust no muestra un solo error ni advertencia; las firmas de tipos encajan perfectamente (fn transfer(from: &mut Account, to: &mut Account, amount: Balance) -> Result<(), TransferError>). Sin embargo, en producción, el sistema acredita el dinero en la cuenta de destino pero un error intermedio de base de datos impide descontarlo de la cuenta de origen.

  • Ejes de análisis:
  1. ¿Qué nivel de verificación garantizó el compilador al compilar la función y qué nivel escapa por completo a su análisis estático?
  2. ¿Qué mecanismo del sistema (transaccionalidad, atomicidad, patrón Saga) es el responsable de garantizar que el balance global se mantenga intacto?
  3. Si una prueba unitaria prueba la función usando mocks en memoria y pasa en verde, ¿por qué esa prueba no constituye evidencia suficiente de que la persistencia ante caídas del servidor sea correcta?
  • Guía de razonamiento: El compilador de Rust garantiza la seguridad de memoria de los punteros en la pila y el heap durante la ejecución del proceso local; no conoce las leyes del universo contable ni el estado de los buffers del disco duro. Distingue siempre la seguridad de tipos (type safety) de la integridad transaccional de los invariantes de negocio.

Pregunta 2: Modelos mentales de gestión de memoria: Pila (Stack), Montículo (Heap) y Propiedad (Ownership)#

En lenguajes con recolector de basura (como JavaScript, Python o Java), puedes crear objetos en cualquier lugar y compartirlos libremente sin preocuparte por quién libera la memoria. En Rust, la memoria de un dato dinámico se libera en el instante exacto en que la variable que posee su propiedad sale de su ámbito de visibilidad (scope).

  • Ejes de análisis:
  1. ¿Qué ocurre físicamente en la memoria de la máquina si dos punteros independientes creen ser dueños del mismo bloque en el heap y ambos intentan liberarlo (double-free)?
  2. ¿Por qué el recolector de basura tradicional introduce pausas de ejecución (stop-the-world) o sobrecarga de metadatos, y cómo logra Rust evitar tanto las fugas de memoria como esas pausas en tiempo de ejecución?
  3. Si necesitas que dos partes de tu programa lean el mismo valor de texto sin duplicar sus bytes en el heap, ¿qué abstracción de Rust debes invocar y qué restricción temporal impone el compilador?
  • Guía de razonamiento: Rastrea siempre la memoria física: visualiza los punteros, las capacidades y los bytes reales. El modelo mental de Rust no es una lista de reglas sintácticas caprichosas; es una solución formal al problema de la gestión determinista de recursos sin sobrecarga en tiempo de ejecución.

Pregunta 3: Contratos en los límites del sistema: validación defensiva frente a parsing tipado#

Un cliente web envía una solicitud HTTP con un cuerpo JSON que contiene un campo {"age": -5}. Un desarrollador novato escribe código que lee directamente ese JSON y realiza operaciones aritméticas, provocando fallos extraños varias capas más adelante en la base de datos.

  • Ejes de análisis:
  1. ¿Por qué asumir que los datos provenientes de la red, del usuario o de una API externa son válidos constituye una vulnerabilidad arquitectónica crítica?
  2. ¿Cuál es la diferencia conceptual entre "validar" un dato (comprobar una condición con un if disperso en medio de la lógica) y "parsearlo" hacia un tipo de dominio estricto que hace imposible representar estados inválidos (Parse, don't validate)?
  3. ¿En qué punto exacto de la arquitectura debe residir la aduana que rechaza el valor -5 y qué código HTTP debe responderse al cliente según el contrato REST?
  • Guía de razonamiento: El borde del sistema es una zona de desconfianza total. Todo byte externo es hostil hasta que un parser demuestre lo contrario. Una vez que el dato cruza el límite y se transforma en un tipo de dominio robusto, el resto del software interno puede operar con certeza absoluta sin re-validar continuamente.

Pregunta 4: Evidencia observable frente a la ilusión de competencia#

Un estudiante copia un fragmento de código sugerido por un modelo de lenguaje de IA. El código compila, se ejecuta y la pantalla muestra el resultado esperado. El estudiante concluye: "Entiendo perfectamente cómo funciona la concurrencia asíncrona en Rust".

  • Ejes de análisis:
  1. ¿Qué experimento adverso o prueba de falsación mínima tendría que realizar ese estudiante para demostrar empíricamente que comprende el mecanismo y no solo el resultado cosmético?
  2. ¿Qué ocurre si se introduce una latencia artificial de red, una desconexión abrupta o una contienda entre hilos en ese fragmento? ¿Podría el estudiante predecir el comportamiento resultante?
  3. ¿Cómo distingue el método científico entre la "familiaridad pasiva" (reconocer una solución cuando se nos presenta) y el "dominio operativo" (capacidad de predecir, falsar, reconstruir y depurar una estructura desde primeros principios)?
  • Guía de razonamiento: La verdadera comprensión técnica se evidencia en la capacidad de predecir con exactitud cómo y por qué fallará el sistema cuando se alteren sus premisas fundamentales. Si no puedes explicar qué hace el código ante un fallo o por qué se eligió una estructura sobre otra, la aparente maestría es solo una ilusión temporal.

Cómo avanzar#

Avanza con paso deliberado y constante:

  1. Completa la práctica sin mirar soluciones: Enfréntate a la pantalla en blanco. Escribir el código con tus propias manos obliga a tu cerebro a activar las vías neuronales de recuperación y síntesis, en lugar del mero reconocimiento pasivo.
  2. Provoca y analiza el fallo: Antes de celebrar una prueba que pasa en verde, altera la condición de corte para forzar un fallo en rojo. Comprueba que el mensaje de error describa con precisión lo que rompiste.
  3. Explica la causa raíz: Si un programa se comporta de forma inesperada o el compilador rechaza tu diseño, detén la escritura. No pruebes cambios aleatorios con la esperanza de que compile. Abre tu libreta o terminal, dibuja el estado de las variables y localiza la regla de invariante violada.
  4. Registra tus dudas abiertas: El aprendizaje genuino no es una lista de éxitos ininterrumpidos; es la acumulación sistemática de hipótesis puestas a prueba y preguntas conceptuales clarificadas.
Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)