Crea tu propio servicio de transferencia de archivos: tutorial de desarrollo
Construye un servicio de transferencia de archivos desde cero. Diseño de API backend, integración de almacenamiento y tutorial de desarrollo de interfaz.
Para construir tu propio servicio de transferencia de archivos necesitas cinco piezas: un frontend que divide y opcionalmente cifra los archivos en el navegador, una API REST que crea sesiones de subida y firma URLs de almacenamiento, almacenamiento de objetos compatible con S3 (Cloudflare R2, Backblaze B2 o MinIO), una pequeña base de datos de metadatos (SQLite o Postgres) y un proxy inverso con TLS 1.3. Con Node 22, Fastify y Vite puedes desplegar un servicio al estilo WeTransfer en unas 600 líneas de código y alojar todo por entre 5 y 15 € al mes con tráfico moderado.
Elegir la capa de almacenamiento primero
La economía del almacenamiento puede arruinar un servicio de transferencia. AWS S3 cobra 0,09 $ por GB de egreso, lo que significa que una descarga popular de 2 GB compartida 1.000 veces te cuesta 180 $ solo en ancho de banda. Cloudflare R2 cobra cero por egreso y 0,015 $ por GB almacenado. Backblaze B2 cobra 0,01 $ por GB de egreso y 0,006 $ por GB almacenado. Para un servicio de transferencia gratuito, R2 es la opción obvia. MinIO te permite alojar la semántica de S3 en tu propio hardware, lo que funciona si ya tienes ancho de banda y discos sin usar.
Usa URLs prefirmadas para que los clientes suban y descarguen directamente del almacenamiento, nunca a través de tu servidor. Esto mantiene tu API limitada por CPU en lugar de por ancho de banda.
Definir la superficie de la API
Mantén la API pequeña. Cinco endpoints cubren el 95% de lo que necesitas:
POST /sessions— crea una sesión de subida, devuelve ID y URLs multipart prefirmadasPOST /sessions/:id/complete— finaliza una subida multipart, devuelve URL de comparticiónGET /shares/:id— devuelve metadatos del archivo (tamaño, caducidad, descargas restantes)GET /shares/:id/download— devuelve una URL de descarga prefirmadaDELETE /shares/:id— permite al propietario revocar antes de tiempo (requiere token de revocación)
Limita agresivamente POST /sessions: 20 por IP y hora es suficiente. Todos los endpoints devuelven JSON, usan Cache-Control: no-store y establecen Content-Security-Policy en las páginas HTML.
Conectar el backend con Fastify
Fastify te da validación de esquema JSON, registro sensato y 30.000 peticiones/s por núcleo sin esfuerzo. Un POST /sessions mínimo:
fastify.post('/sessions', {
schema: {
body: {
type: 'object', required: ['size'],
properties: {
size: { type: 'integer', minimum: 1, maximum: 10737418240 },
contentType: { type: 'string', maxLength: 120 }
}
}
}
}, async (req) => {
const id = nanoid(16);
const key = `blobs/${id}`;
const upload = await s3.createMultipartUpload({
Bucket: 'transfers', Key: key, ContentType: req.body.contentType
});
const partCount = Math.ceil(req.body.size / (8 * 1024 * 1024));
const urls = await Promise.all(
Array.from({ length: partCount }, (_, i) =>
getSignedUrl(s3, new UploadPartCommand({
Bucket: 'transfers', Key: key, UploadId: upload.UploadId, PartNumber: i + 1
}), { expiresIn: 3600 }))
);
db.prepare('INSERT INTO sessions VALUES (?, ?, ?, ?)')
.run(id, upload.UploadId, key, Date.now());
return { id, uploadId: upload.UploadId, partUrls: urls };
});
Techo de 10 GB, partes de 8 MB, validez de URL de 1 hora. Ajusta según tu mix de tráfico.
Construir la interfaz de subida
Vite más React sin ninguna biblioteca de UI llega a un arrastrar y soltar funcional en unas 150 líneas. La pieza crítica es el worker de subida: lanza un Web Worker dedicado, pásale el handle del File y las URLs de partes, y haz que haga PUT de cada fragmento con concurrencia de 3. El hilo principal se mantiene reactivo y las actualizaciones de progreso llegan vía postMessage.
// worker.js
self.onmessage = async ({ data: { file, partUrls } }) => {
const partSize = 8 * 1024 * 1024;
const etags = [];
for (let i = 0; i < partUrls.length; i++) {
const blob = file.slice(i * partSize, (i + 1) * partSize);
const res = await fetch(partUrls[i], { method: 'PUT', body: blob });
etags[i] = res.headers.get('etag');
self.postMessage({ type: 'progress', done: i + 1, total: partUrls.length });
}
self.postMessage({ type: 'done', etags });
};
Añade un campo de contraseña que derive una clave mediante PBKDF2 (600.000 iteraciones, SHA-256) y envuelva cada fragmento con AES-256-GCM antes del PUT. Eso convierte tu servicio en uno con cifrado de extremo a extremo en lugar de cifrado en el servidor.
Diseñar el esquema de la base de datos
SQLite a través de better-sqlite3 gestiona decenas de millones de filas sin problemas. Mantén el esquema mínimo:
CREATE TABLE sessions (
id TEXT PRIMARY KEY, upload_id TEXT, object_key TEXT, created_at INTEGER
);
CREATE TABLE shares (
id TEXT PRIMARY KEY, object_key TEXT, size_bytes INTEGER,
content_type TEXT, expires_at INTEGER, max_downloads INTEGER,
download_count INTEGER DEFAULT 0, revocation_token TEXT
);
CREATE INDEX idx_shares_expires ON shares(expires_at);
Sin tabla de usuarios, sin correo, sin registros de IP. Esa es una decisión deliberada: hace que el cumplimiento del RGPD sea casi trivial. Un cron nocturno elimina las filas caducadas y sus objetos S3:
const expired = db.prepare('SELECT object_key FROM shares WHERE expires_at < ?').all(Date.now());
for (const { object_key } of expired) {
await s3.deleteObject({ Bucket: 'transfers', Key: object_key });
}
db.prepare('DELETE FROM shares WHERE expires_at < ?').run(Date.now());
Implementar enlaces de compartición y descargas
Las URLs de compartición tienen la forma https://tuapp.com/f/abc123#k=<clave_base64> cuando has añadido cifrado del lado del cliente. El fragmento mantiene la clave fuera de los registros del servidor. La página de descarga obtiene /shares/abc123, consigue una URL de S3 prefirmada, transmite bytes directamente desde R2, descifra en el navegador y entrega el resultado a StreamSaver.js o a la File System Access API.
Establece Content-Disposition: attachment; filename*=UTF-8''nombre-codificado para que el navegador active un diálogo de guardado en lugar de renderizar el archivo en línea. Esto es importante para PDFs, archivos HTML y SVG, todos los cuales pueden ejecutar scripts en un contexto de navegador.
Protección contra el abuso
Los servicios de transferencia atraen abusadores. Espera tres categorías: distribución de malware, alojamiento de cargas de phishing y subidas que infringen derechos de autor. Mitigaciones por orden de impacto: limitar subidas por IP, exigir un tamaño mínimo (menos de 10 KB es casi siempre basura), calcular un SHA-256 del ciphertext y compararlo con una lista de hashes conocidos como maliciosos, y proporcionar un formulario de reporte de abuso sin fricción. INCIBE y la AEPD recomiendan mecanismos de reporte accesibles como parte de las obligaciones RGPD para plataformas de intercambio de contenido.
ClamAV en contenido descifrado solo funciona cuando tienes la clave, lo que anula el cifrado de extremo a extremo: la respuesta correcta son los límites de tasa agnósticos al contenido más eliminaciones reactivas.
Desplegar y ejecutar de forma económica
Un solo servidor Hetzner CX22 de 10 € (2 vCPU, 4 GB RAM) ejecuta Fastify + SQLite + un proxy inverso Caddy y gestiona miles de usuarios activos. Caddy te da TLS 1.3 automáticamente vía Let's Encrypt. Pon Cloudflare por delante para protección DDoS y el WAF gratuito. Monitoriza con Uptime Kuma (gratuito, autoalojado) y registra en stdout capturado por journald.
Para redundancia geográfica, replica SQLite con Litestream a R2 y apunta una segunda región al mismo almacén de objetos. La conmutación por error es a nivel de DNS. Tu historia de recuperación ante desastres cabe en una servilleta.
Todo el enfoque anterior refleja cómo está construido HexaTransfer: API Fastify, almacenamiento R2, Web Crypto en el navegador, metadatos SQLite, sin cuentas. Pruébalo en https://hexatransfer.com — gratuito, sin cuenta, hasta 10 GB.
Qué omitir hasta que lo necesites
No construyas cuentas, equipos ni carpetas en la v1. No añadas envío de correo. No implementes vista previa de vídeo o miniaturas de imagen (ambas obligan al servidor a ver el texto claro). No lances una aplicación móvil antes de que la versión web funcione correctamente en Safari móvil. La expansión de funcionalidades que mató a los primeros clones de servicios de transferencia siempre seguía el mismo patrón: cuentas, luego cuotas de almacenamiento, luego pago, luego planes de equipo, y de repente competías con Dropbox en lugar de ofrecer una herramienta de transferencia rápida y privada.
Envía archivos grandes de forma segura con cifrado de extremo a extremo
Transfiere archivos de hasta 10 GB gratis con cifrado de extremo a extremo. Sin necesidad de cuenta. Tus archivos se cifran en tu navegador antes de subirlos: nadie más puede leerlos.
Enviar un archivo