Cómo distribuir aplicaciones de escritorio creadas por desarrolladores solitarios sin advertencias de seguridad
Si has terminado una aplicación de escritorio que funciona bien localmente, solo has completado la mitad del desarrollo. El verdadero obstáculo comienza en el momento en que aparece una ventana de advertencia roja, como "archivo dañado" o "Windows protegió su PC", en la pantalla del usuario que acaba de hacer clic en el enlace de descarga.
Superar las barreras de seguridad del sistema operativo, controlar la velocidad de compilación y crear un sistema de actualización automática que se encargue de las versiones más recientes cada vez que el usuario abre la aplicación es más agotador de lo que parece. Incluso si elegiste Tauri v2 por ser más ligero que Electron, los problemas prácticos del proceso de distribución siguen ahí. He resumido cómo los desarrolladores solitarios o los equipos pequeños pueden distribuir sus productos de forma impecable sin desperdiciar tiempo ni dinero innecesario.
1. Utilizar firmas en la nube en lugar de certificados de 700 dólares al año
Una aplicación de escritorio sin firma de código (Code Signing) es tratada como malware a nivel del sistema operativo. Para evitar mostrar advertencias de seguridad en la PC del usuario, se necesita dinero y papeleo.
macOS: Configuración de Apple Developer Account y Entitlements
Para distribuir en macOS, es obligatorio inscribirse en el Apple Developer Program, que cuesta 99 dólares al año. Una vez obtenida la cuenta, debes crear un archivo src-tauri/Entitlements.plist que defina los permisos de excepción de seguridad de memoria para que el webview de Tauri funcione correctamente. Si falta esta configuración, la aplicación se cerrará inmediatamente al iniciarse.
`xml
com.apple.security.cs.allow-jit
com.apple.security.cs.allow-unsigned-executable-memory
`
Especifica este archivo en las opciones de bundle de src-tauri/tauri.conf.json.
`json
{
"bundle": {
"macOS": {
"signingIdentity": "Developer ID Application: Your Name (TEAMID)",
"entitlements": "./Entitlements.plist",
"minimumSystemVersion": "11.0",
"dmg": {
"appPosition": { "x": 180, "y": 170 },
"applicationFolderPosition": { "x": 480, "y": 170 }
}
}
}
}
`
Windows: Ahorrar costos con Azure Trusted Signing
Para pasar el filtro SmartScreen de Windows, anteriormente era necesario obtener un certificado EV (Extended Validation), que costaba entre 400 y 700 dólares al año, en forma de token USB físico. No solo es costoso, sino que es extremadamente engorroso de gestionar para una persona.
La alternativa es Azure Trusted Signing (ATS), el servicio de firma basado en la nube de Microsoft. Si pagas una suscripción mensual de unos 9.99 dólares, Microsoft gestiona la firma dentro de su nube HSM, por lo que no es necesario guardar ninguna llave física.
- Crea una cuenta de Azure Trusted Signing y un perfil de certificado en el portal de Azure.
- Ingresa la información de suscripción de Azure (
AZURE_TENANT_ID, CLIENT_ID, CLIENT_SECRET) y la información de ATS en las variables de entorno (Secrets) de GitHub Actions.
- Ejecuta
sign-tool durante el proceso de compilación para aplicar la firma digital al archivo MSI o EXE compilado por Tauri.
Una aplicación firmada de esta manera evita las advertencias de Windows SmartScreen desde el momento de la primera descarga, lo que permite retener a los usuarios que, de otro modo, abandonarían en la etapa de instalación.
2. Cómo acelerar la compilación en GitHub Actions en un 90%
Tauri es ligero, pero en el proceso de compilación debe ejecutar el compilador de Rust y las cadenas de herramientas nativas de cada sistema operativo. Aunque funcione bien en tu computadora, es común que ocurran errores de enlace en la computadora de otro miembro del equipo o que la distribución se corrompa debido a la contaminación de dependencias del entorno local. La compilación para distribución debe realizarse obligatoriamente en un pipeline de CI/CD aislado para mayor seguridad.
El problema es que el runner alojado por defecto en GitHub Actions no tiene las especificaciones suficientes para compilar Rust. Si se trata de una estructura que descarga las dependencias y compila desde cero cada vez, es fácil que una compilación de lanzamiento tarde más de 10 minutos.
En este caso, en lugar de usar actions/cache para comprimir y enviar archivos a la nube, la combinación de un plugin de caché dedicado que funciona con almacenamiento NVMe de alto rendimiento (swatinem/rust-cache) o runners alojados dedicados (Namespace, Depot, etc.) cambiará la velocidad drásticamente.
Basado en los registros de compilación del proyecto de código abierto del reproductor de música spotify-player, el resultado de la comparación de rendimiento entre un runner estándar de GitHub y un runner dedicado con caché de volumen local es el siguiente:
| Plataforma y configuración de caché |
Tiempo requerido runner estándar de GitHub |
Tiempo requerido con optimización de caché |
Tasa de reducción de tiempo de compilación |
| Ubuntu Linux |
9 min 31 seg |
34 seg |
94.0% reducción |
| macOS Darwin |
9 min 31 seg |
27 seg |
95.2% reducción |
| Windows MSVC |
9 min 31 seg |
44 seg |
92.2% reducción |
| Costo del flujo de trabajo |
$0.44 por ejecución |
$0.074 por ejecución |
83.1% ahorro |
Simplemente conectando una infraestructura de caché de volumen persistente, el tiempo de espera de compilación del equipo de desarrollo se reduce en al menos un 40%.
La configuración de automatización de distribución se especifica en .github/workflows/publish.yml de la siguiente manera.
`yaml
jobs:
build-binaries:
strategy:
matrix:
platform: [macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
# ... después de los pasos de compilación, llamar a tauri-action
`
Al poner tauri-apps/tauri-action al final del flujo de trabajo, cada vez que envíes una nueva etiqueta (push tag), los instaladores firmados para ambos SO se registrarán automáticamente en GitHub Release Draft.
3. Aislamiento de datos SQLite en preparación para el inicio de sesión webview
Al empaquetar el instalador para Windows, debes decidir el método de instalación del motor de webview, WebView2. Si se garantiza la conexión a Internet y el entorno requiere reducir drásticamente el tamaño del archivo de descarga, el método downloadBootstrapper es ideal, ya que no aumenta el tamaño del paquete. Por el contrario, si te diriges a redes cerradas o entornos sin conexión, es más seguro incluir el offlineInstaller, aunque se añadan unos 127 MB al archivo de instalación.
La forma en que manejas los datos al operar una aplicación basada en Tauri también es importante. Es peligroso simplemente guardar datos en IndexedDB o LocalStorage, que son almacenamiento del navegador.
De hecho, al pasar de Tauri v1 a v2, hubo un cambio interno en el esquema de dominio del webview en el entorno de Windows, de [https://tauri.localhost](https://tauri.localhost) a [http://tauri.localhost](http://tauri.localhost). Debido a esto, hubo muchos casos en los que se perdieron todos los datos existentes debido a que la ruta de la caché del navegador se cambió a la fuerza.
Para evitar el gran desastre de que los datos se inicialicen después de la distribución, la información principal debe almacenarse directamente como un archivo SQLite en el área del sistema de archivos nativo en lugar de en el almacenamiento del webview. Si usas la API appDataDir de Tauri v2, automáticamente encontrará una ruta de sandboxing segura que cumpla con los estándares del sistema operativo.
- Windows:
C:\Users\<UserName>\AppData\Roaming\<BundleIdentifier>
- macOS:
/Users/<UserName>/Library/Application Support/<BundleIdentifier>
Un ejemplo de cómo intervenir en el ciclo de vida de la aplicación dentro del código Rust (src-tauri/src/lib.rs), que es el backend de Tauri v2, para vincular de forma segura la base de datos SQLite al área segura y ejecutar la migración del esquema es el siguiente:
`rust
use std::fs;
use tauri::Manager;
use tauri_plugin_sql::{Migration, MigrationKind};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let database_migrations = vec![
Migration {
version: 1,
description: "initialize_user_profiles_table",
sql: "CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);",
kind: MigrationKind::Up,
}
];
tauri::Builder::default()
.setup(|app| {
let local_app_dir = app.path().app_data_dir()
.expect("Critical: Could not resolve target operating system app data path.");
if !local_app_dir.exists() {
fs::create_dir_all(&local_app_dir)
.expect("Critical: Failed to establish persistent storage directory structure.");
}
Ok(())
})
.plugin(
tauri_plugin_sql::Builder::default()
.add_migrations("sqlite:users.db", database_migrations)
.build()
)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
`
Al configurarlo de esta manera, incluso si la caché interna de Electron o Chromium Webview se elimina por completo debido a una actualización automática o reinstalación, la base de datos del usuario real se conservará de forma segura.
4. Construcción de actualizaciones automáticas en segundo plano
El método de inducir a los usuarios a visitar la página de inicio y descargar la nueva versión cada vez aumenta la tasa de abandono. Debes establecer una estructura que sirva los archivos de actualización silenciosamente combinando almacenamiento de objetos en la nube y CDN.
La combinación de Cloudflare R2 y AWS CloudFront es eficiente como servidor de distribución. Cloudflare R2 no tiene tarifas de egreso (Egress Fees), por lo que puedes fijar en cero el costo del tráfico de red generado al publicar archivos de actualización a gran escala.
Política de control de caché de CDN
El archivo de metadatos (latest.json) que el cliente consulta para verificar si hay una nueva versión no debe ser almacenado en caché por la CDN o el navegador. Debes especificar la siguiente política en el encabezado de respuesta:
`http
Cache-Control: no-cache, no-store, must-revalidate
`
Por otro lado, dado que los archivos binarios de instalación reales son inmutables (Immutable) y contienen valores hash únicos, configúralos para que la CDN los mantenga durante el mayor tiempo posible para reducir la carga de tráfico del servidor original.
`http
Cache-Control: public, max-age=31536000, immutable
`
Cambios en la configuración del actualizador de Tauri v2
En Tauri v2, la ubicación de las opciones relacionadas con la actualización se ha movido debajo del bloque plugins.updater. A continuación, la especificación de configuración de tauri.conf.json:
`json
{
"bundle": {
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"active": true,
"endpoints": [
"https://cdn.myapp.com/releases/latest.json"
],
"dialog": false,
"pubkey": "dW5zaWduZWQgYm91bmRmaXg...",
"windows": {
"installMode": "passive"
}
}
}
}
`
Para hacer que el usuario actualice sin tener que hacer clic en molestas ventanas de confirmación en el entorno de Windows, debes establecer el installMode en passive o quiet. El modo passive muestra una barra de progreso tranquila en lugar de la ventana del asistente de instalación y luego completa el reemplazo silenciosamente.
Una vez finalizada la configuración, vincula @tauri-apps/plugin-updater y @tauri-apps/plugin-process en el área de frontend para verificar nuevos parches al momento de ejecutar la aplicación e inducir al reinicio.
`typescript
import { check } from "@tauri-apps/plugin-updater";
import { ask } from "@tauri-apps/plugin-dialog";
import { relaunch } from "@tauri-apps/plugin-process";
export async function runBackgroundUpdater(): Promise {
try {
const updatePayload = await check();
if (updatePayload && updatePayload.available) {
const userResponse = await ask(
`Una nueva versión [v${updatePayload.version}] está disponible. ¿Desea actualizar y reiniciar la aplicación ahora?`,
{
title: "Aviso de actualización automática de software",
kind: "info",
okLabel: "Instalar actualización y reiniciar",
cancelLabel: "Aplicar más tarde"
}
);
if (userResponse) {
await updatePayload.downloadAndInstall();
await relaunch();
}
}
} catch (error) {
console.error("Excepción durante el proceso de verificación de actualización automática:", error);
}
}
`
Simplemente insertando esta función en la fase de montaje inicial del componente de React o Vue de nivel superior, los usuarios siempre usarán el software en su versión más reciente sin necesidad de buscar en la página de inicio ellos mismos.
5. Resumen
- Reducción de costos: Para un equipo de desarrollo de una sola persona, es más realista aplicar Azure Trusted Signing, que cuesta unos 10 dólares al mes, en lugar de certificados EV que cuestan cientos de dólares cada año, y combinarlo con Cloudflare R2, que no tiene comisiones por tráfico de descarga.
- Preservación de datos: No es necesario depender del almacenamiento local del navegador, que es propenso a perderse cuando la sesión expira o cambia la especificación del dominio. Debes mantener el archivo de base de datos SQLite bajo la ruta
appDataDir controlada por el nativo y ejecutar migraciones de esquema a largo plazo para que los datos no se corrompan durante las actualizaciones de la aplicación.
- Velocidad de distribución: No compiles en tu propia computadora para subirlo manualmente; debes configurar una matriz de compilación (build matrix) a la que se le apliquen técnicas de caché de runner para automatizar la versión de lanzamiento y evitar estrés en la etapa de creación del paquete.