La primera vez que alguien me describió Tauri, dijo que construyes una aplicación de escritorio usando HTML, CSS y JavaScript. Mi reacción fue escepticismo. Eso suena como una página web en una ventana, no a una aplicación real. Luego señal que VS Code, Slack y Discord están todos construidos de la misma manera, usando Electron, que es la versión más antigua y popular de este enfoque. Esas son aplicaciones reales, usadas por millones de personas cada día. La arquitectura tiene un nombre: aplicaciones de escritorio híbridas. No es un hack, y no es nueva.
Funciona así:
Capa de UI (tecnologías web)
↓
Puente nativo
↓
Capacidades del OSTu interfaz está construida con la plataforma web. Un puente la conecta al sistema operativo. El puente es lo que convierte una página web en una aplicación de escritorio.
Electron demostró que esto funciona
Probablemente has usado aplicaciones Electron sin darte cuenta. VS Code, Slack, Discord y una larga lista de otras herramientas de escritorio están construidas con ello. Electron tomó la posición de que empaquetar un motor de navegador completo dentro de cada app era un precio razonable por permitir a los desarrolladores web lanzar software de escritorio sin aprender un stack nuevo.
HTML / CSS / JS
↓
Puente Chromium + Node
↓
OSEl tradeoff es el tamaño. Cada app de Electron envía su propia copia de Chromium y Node. Una pequeña aplicación Electron puede ocupar 150 megabytes en disco porque contiene un navegador entero. El uso de memoria sigue el mismo patrón: cada ventana de Electron trae su propio proceso Chromium.
Tauri toma la misma filosofía pero hace tradeoffs diferentes:
HTML / CSS / JS
↓
Tauri IPC
↓
Rust
↓
OSEn lugar de empaquetar un navegador, Tauri usa el WebView que ya está instalado en el sistema del usuario. En Windows es WebView2, en macOS es WKWebView, en Linux es WebKitGTK. En lugar de Node, el backend es Rust.
| Electron | Tauri | |
|---|---|---|
| Motor de navegador | Chromium empaquetado | WebView del sistema |
| Backend | Node.js | Rust |
| Tamaño de la app | Grande (100+ MB) | Pequeño (típico 2-10 MB) |
| Modelo de seguridad | Node tiene acceso total al sistema | El frontend no es confiable por defecto |
| Memoria | Mayor (Chromium completo por app) | Menor (comparte WebView del sistema) |
La diferencia de tamaño es lo que la mayoría de la gente nota primero. El modelo de seguridad es la distinción más interesante, y voy a llegar a ello.
Un proceso, no dos
Cuando empecé a leer sobre Tauri, seguía buscando la parte donde el frontend se conecta al backend. Llevaba el modelo web en la cabeza, donde el navegador habla con un servidor sobre HTTP, y asumí que Tauri tendría algo similar. Una API de localhost, quizás, o algún tipo de servidor embebido con el que hablara el WebView.
Eso no es lo que ocurre.
Cuando ejecutas:
pnpm tauri devarranca un ejecutable de Rust. Ese ejecutable hace cuatro cosas:
- Crea una ventana nativa del OS
- Crea un WebView dentro de esa ventana
- Carga tu frontend en el WebView
- Crea un puente IPC entre el WebView y el código Rust
Ejecutable de Rust
├── ventana nativa
├── WebView (tu app React/Vue/Svelte)
└── puente IPCNo hay un servidor frontend separado. No hay una API de localhost. No hay un proceso de backend separado. El proceso de Rust es el orquestador. Crea la ventana, carga la UI web en ella y espera para manejar mensajes de ella.
Esa fue la parte con la que tuve que quedarme un rato. En una aplicación web, el navegador y el backend son dos programas separados corriendo en máquinas distintas. El navegador descubre el backend a través de una URL. En Tauri son un programa. El "backend" es simplemente el código Rust corriendo en el mismo proceso que creó el WebView en primer lugar. Si has usado Next.js, esto se parece más a eso. Tu frontend y tus rutas de API son un proyecto, un proceso, con el framework gestionando el enrutamiento. No despliegas un frontend y un backend separados. Tauri tiene la misma forma.
Cómo JavaScript habla con Rust
Viniendo del desarrollo web, esperarías simplemente importar algo y llamarlo. Así es como funciona todo en JavaScript. Instalas un paquete, lo importas, lo usas.
Tauri te da un helper que se ve exactamente así:
import { invoke } from "@tauri-apps/api/core";
const result = await invoke("greet", { name: "John" });Y en la superficie, eso es lo que es. Llamas a algo, recibes un resultado. Pero por debajo, nada de esto funciona como reconocerías de la web. No hay un módulo compartido. No hay una función importada. Estás enviando un mensaje a otro proceso y esperando a que responda.
JavaScript
|
| mensaje: { command: "greet", args: { name: "John" } }
|
puente IPC
|
despachador de comandos de Rust
|
fn greet(name: String) -> String
|
| devuelve: "Hello, John"
|
Promise de JavaScript se resuelveEn el lado de Rust, la función que recibe esto se ve así:
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}", name)
}El atributo #[tauri::command] registra la función con el despachador IPC. Cuando JavaScript llama invoke("greet", ...), el despachador encuentra esta función, pasa los argumentos y devuelve el resultado. El lado de JavaScript obtiene una Promise. El lado de Rust ejecuta una función compilada real.
El nombre para esto es IPC, Comunicación entre Procesos. La razón por la que existe es que dos procesos no pueden alcanzar directamente la memoria del otro. Tienen que comunicarse a través de mensajes. Lo que ves arriba es una forma estructurada de que dos procesos intercambien datos sin compartir memoria.
Hubo una parte de esto que me tropezó. En una aplicación web, el navegador habla con el backend sobre HTTP, lo que significa que necesita un puerto, algo como localhost:3000. En Tauri, no hay puerto. Seguía buscando dónde se establecía la conexión y no podía encontrarla, porque no hay conexión. El puente IPC es creado internamente por el runtime de Tauri. Es como dos habitaciones en el mismo edificio. No llamas a alguien por teléfono cuando está en la habitación de al lado. Usas el tubo de correo interno.
El puente ya está ahí
Una vez que entendí que no había puerto, la siguiente pregunta era obvia: si no hay servidor, ¿cómo encuentra JavaScript a Rust?
La respuesta es que no lo encuentra. No tiene que hacerlo.
Cuando el proceso de Rust crea el WebView, inyecta el puente de comunicación en él. Conceptualmente:
Runtime de Rust
|
| inyecta
|
window.__TAURI__Cuando tu JavaScript llama invoke(), usa ese puente. El frontend no descubre a Rust. No hay una URL a la que apuntar, ningún puerto al que conectarse. Rust construyó el entorno en el que el frontend corre, y el canal de comunicación era parte de ese entorno antes de que se cargara la primera línea de JavaScript.
Esa es una diferencia clara del modelo web. En la web, el navegador descubre el backend a través de una URL. En Tauri, el backend creó el entorno en el que el frontend corre, y el canal de comunicación era parte de ese entorno desde el principio.
Dónde vive el límite de confianza
El frontend no es confiable. Eso es cierto para cada frontend, y no es la parte interesante. La parte interesante es dónde cada framework pone el límite de confianza y qué te permite scopear.
En Electron, el backend de Node tiene acceso total al sistema por defecto. Si el frontend obtiene ese acceso es una bandera de configuración, nodeIntegration. Ponla en true y tu renderer puede llamar child_process, fs, cualquier cosa que Node pueda hacer. Ponla en false y el renderer está en sandbox, pero el proceso principal sigue teniendo acceso total, y típicamente expones lo que necesitas a través de ipcMain.handle. El límite es "¿tiene el frontend la API de Node o no?" Es una puerta binaria. No hay un sistema de capacidades que diga que el frontend puede llamar exportReport pero no deleteFile. Escribes esa lógica tú mismo en cada manejador IPC.
En Tauri, el frontend tiene acceso cero por defecto. Cada comando que el frontend puede llamar debe estar explícitamente registrado como un #[tauri::command], y el sistema de capacidades controla qué comandos puede invocar el frontend. Solo el lado de Rust tiene acceso al OS. El sistema de capacidades scopea lo que el frontend puede pedir. No escribes un guard en cada manejador. El framework lo hace cumplir.
| Electron | Tauri | |
|---|---|---|
| Acceso al OS del backend | Total por defecto | Total, pero solo Rust lo tiene |
| Acceso del frontend al OS | Bandera de configuración (nodeIntegration) | Ninguno por defecto |
| Lo que el frontend puede llamar | Lo que expongas vía ipcMain.handle | Solo comandos registrados con #[tauri::command] |
| Scoping por comando | Escribes la lógica del guard tú mismo | El sistema de capacidades scopea cada comando |
La analogía del restaurante sigue vigente. El personal del comedor pone pedidos en una vía de tickets. La cocina decide qué hacer y cómo hacerlo. Pero en Electron, la puerta de la cocina tiene cerradura, y tienes que recordar cerrarla y decidir quién recibe una llave. En Tauri, la puerta de la cocina empieza cerrada, y cada plato específico que el comedor puede pedir tiene que ser añadido al menú explícitamente. El valor predeterminado es "nada está en el menú." Añades elementos uno a la vez, y el framework lo hace cumplir.
El build
Durante el desarrollo, dos cosas corren al mismo tiempo:
Frontend: Vite dev server (hot reload, fast refresh)
Backend: Cargo (compilador de Rust, vigilando cambios)Editas tus componentes React, Vite los recarga en caliente. Editas tu código Rust, Cargo recompila. La ventana nativa se mantiene abierta mientras ambos lados se actualizan.
En producción, el frontend se construye y Rust se compila en un binario de release. Los dos se empaquetan juntos:
Build del frontend
+
Build de release de Rust
|
v
Aplicación nativaEl output depende del objetivo:
| Plataforma | Output |
|---|---|
| Windows | .exe / .msi |
| macOS | .app / .dmg |
| Linux | AppImage / .deb |
| Android | .apk / .aab |
| iOS | .ipa |
Tauri móvil vale la pena mencionarlo aquí. En móvil, la arquitectura es la misma: tu UI web corre en el WebView de la plataforma, Android WebView o iOS WKWebView, y Rust se compila nativamente para la plataforma móvil. No es React Native. No hay puente de JavaScript a UI nativa. La UI es un WebView, y Rust maneja el lado nativo.
Navegador vs Tauri
Tu app de React puede correr en un navegador normal con pnpm dev. Pero invoke() no funcionará, porque no hay runtime de Rust ni puente IPC. La función existe en el paquete de JavaScript, pero cuando intenta enviar un mensaje a través del puente, no hay nada al otro lado.
En Tauri, el runtime de Rust está corriendo, el puente existe, y invoke() lleva mensajes a funciones reales de Rust. Esa es la diferencia entre abrir tu app en Chrome y correrla a través de Tauri. El código es el mismo. El runtime es diferente.
Cuándo encaja
Tauri funciona bien para:
- Apps de productividad de escritorio
- Herramientas de desarrollador
- Software empresarial
- Apps para tomar notas
- Utilidades de archivos
- Herramientas internas de empresa
- Utilidades multiplataforma que necesitan correr en Windows, macOS y Linux
Es menos ideal para:
- Juegos 3D de alto rendimiento
- Aplicaciones pesadas en gráficos donde necesitas cada detalle de UI nativa
- Apps que necesitan integración profunda específica de plataforma con frameworks de UI a nivel OS
Los buenos encajes comparten algo: la interfaz es el trabajo, y el sistema operativo proporciona capacidades de apoyo como acceso a archivos, notificaciones y gestión de ventanas. Los malos encajes son lo contrario: los gráficos o el sistema de UI de la plataforma nativa es el trabajo, y una UI web se interpone.
El cambio de mentalidad
La pregunta con la que empecé fue si realmente estamos diseñando interfaces en lugar de apps de escritorio. La respuesta es sí, pero el enmarcado importa.
No estás poniendo una página web en una ventana de escritorio. Estás construyendo una aplicación nativa donde la tecnología de UI resulta ser la plataforma web. Esa distinción es lo que hace funcionar la arquitectura. El lado de Rust es un backend nativo real con acceso real al OS. El lado web es la capa de presentación. El puente entre ellos es donde vive el límite de confianza.
Una página web en una ventana sería un navegador apuntando a una URL, sin capacidades nativas. Lo que Tauri te da es una aplicación nativa compilada que resulta renderizar su interfaz con tecnologías web. La diferencia es el backend de Rust, el puente IPC y el modelo de seguridad a su alrededor.
Leer más
Cómo funcionan las apps móviles
Las tres formas de construir una app móvil, qué ejecuta realmente cada una por debajo, y el patrón de arquitectura compartido detrás de React Native, Flutter y Tauri mobile.
Una base de código, Web y Escritorio
Cómo diseñar una app de Tauri para que el mismo código React corra en el navegador y en el escritorio, poniendo las diferencias de runtime detrás de una interfaz de servicio.
Agrupar claves de API con Bifrost
Una guía práctica para configurar la agrupación de claves de API en Bifrost y cómo escalarla de forma segura entre múltiples nodos externalizando tus rate limits.
