wandres.dev
OPFS I · el sistema de archivos del origen

OPFS: el sistema de archivos que el usuario nunca ve

Qué es el sistema de archivos privado del origen, por qué comparte interfaces con la File System Access API pero no su modelo de permisos, y qué implica que no haya ningún diálogo de por medio.

⏱ 14 min

Durante veinte años el navegador te ofreció cajas donde guardar cosas: un diccionario de cadenas, un almacén de objetos, una caché de respuestas HTTP. Nunca te ofreció lo que cualquier programa de escritorio da por sentado desde 1970 —un archivo con un offset, donde puedes reescribir el byte 8192 sin tocar los 8191 anteriores—. OPFS es exactamente eso: un sistema de archivos real, con su árbol de directorios, que aparece sin diálogo, sin ruta visible y sin que el usuario llegue a saber nunca que existe.

🎯 Al terminar esta lección sabrás
  • Distinguir OPFS de la File System Access API: las mismas interfaces sobre dos modelos de seguridad opuestos.
  • Entender qué encierra exactamente la palabra privado en la expresión sistema de archivos privado del origen.
  • Obtener la raíz y comprobar el aislamiento.
  • Situar qué se estudia aquí y qué queda para el nivel 10.

Dos puertas, el mismo tipo de manejador

El File System Standard del WHATWG define una jerarquía muy pequeña de objetos: FileSystemHandle, del que descienden FileSystemFileHandle y FileSystemDirectoryHandle. Un manejador no es una ruta ni un descriptor numérico: es una referencia opaca a una entrada del árbol, y todo lo que puedes hacer con un archivo pasa por tenerla.

Lo interesante es que hay dos formas distintas de conseguir manejadores del mismo tipo, y solo se diferencian en su procedencia:

flowchart TD
A[Tu codigo] --> B[showOpenFilePicker o showDirectoryPicker]
A --> C[navigator.storage.getDirectory]
B --> D[Dialogo del sistema y permiso revocable]
C --> E[Sin dialogo y sin gesto del usuario]
D --> F[Manejador sobre un archivo real del disco]
E --> G[Manejador sobre el arbol privado del origen]
F --> H[Misma interfaz: getFile y createWritable]
G --> H
// Puerta 1: la File System Access API. Exige gesto del usuario y permiso.
const [elegido] = await window.showOpenFilePicker();

// Puerta 2: OPFS. Ni gesto, ni diálogo, ni permiso.
const raiz = await navigator.storage.getDirectory();

raiz.kind;                                   // 'directory'
raiz.name;                                   // '' — la raíz no tiene nombre
elegido instanceof FileSystemFileHandle;     // true

A partir de aquí el código que lee y escribe es literalmente el mismo. La superficie completa cabe en una pantalla, y conviene verla junta desde el principio para calibrar cuánto tendrás que construir tú:

// FileSystemHandle — común a los dos tipos
handle.kind;                    // 'file' | 'directory'
handle.name;
await handle.isSameEntry(otro);

// FileSystemDirectoryHandle
await dir.getFileHandle(nombre, { create: true });
await dir.getDirectoryHandle(nombre, { create: true });
await dir.removeEntry(nombre, { recursive: true });
await dir.resolve(descendiente);
for await (const [nombre, h] of dir.entries()) { /* ... */ }

// FileSystemFileHandle
await file.getFile();           // leer
await file.createWritable();    // escribir

Eso es todo. No hay copiar, ni mover, ni renombrar, ni consultar metadatos sin abrir, ni buscar por patrón. Puedes escribir una capa de persistencia agnóstica que reciba un FileSystemDirectoryHandle y no sepa —ni le importe— si vino de un selector o de la raíz privada. Esa simetría es deliberada y es una de las mejores decisiones de diseño de la plataforma web reciente.

La asimetría está en todo lo demás.

📝
Ni es una API aparte ni se llama como parece

OPFS no es una especificación propia ni un objeto global: es una sección del File System Standard, y su único punto de entrada es un método de navigator.storage, el mismo objeto que gobierna la cuota. Esa colocación no es casual y ya te dice mucho: para el navegador, tu árbol de ficheros no es un dispositivo montado, es una partida más de la contabilidad de almacenamiento del origen, sujeta a las mismas reglas que IndexedDB.

Qué significa privado del origen

El origen es la tupla esquema más host más puerto. https://app.ejemplo.com y https://www.ejemplo.com son orígenes distintos, y por tanto tienen árboles distintos, sin ningún puente entre ellos: ni lectura, ni listado, ni prueba de existencia. No hay una raíz común más arriba desde la que fisgar.

// El mismo código, dos orígenes: dos árboles que jamás se cruzan.
const raiz = await navigator.storage.getDirectory();
await raiz.getFileHandle('marca.txt', { create: true });

for await (const nombre of raiz.keys()) console.log(nombre);
// en https://app.ejemplo.com  → ['marca.txt']
// en https://www.ejemplo.com  → []

OPFS vive dentro del subsistema de almacenamiento del navegador, junto a IndexedDB y Cache Storage, sujeto a la misma cuota y a las mismas políticas de borrado. No es un montaje del disco del usuario: es un sistema de archivos virtual que el navegador implementa como quiere. Chrome guarda las entradas en su directorio de perfil con nombres opacos, Safari y Firefox lo hacen de otra manera, y nada de eso es contrato.

De hecho ni siquiera está garantizado que exista un fichero real por cada entrada tuya. La especificación describe el comportamiento observable —un árbol, entradas con nombre, bytes con offset— y deja libre la representación: una implementación podría empaquetar entradas pequeñas, comprimirlas o guardarlas dentro de una base de datos interna, y seguiría siendo conforme. Lo único que puedes exigir es la semántica, nunca la forma.

Esa libertad tiene un efecto agradable poco conocido: como los nombres no tienen que sobrevivir al sistema de ficheros anfitrión, OPFS admite nombres que el sistema operativo rechazaría. Puedes llamar a una entrada aux, con o PRN en Windows, o usar caracteres que un sistema real prohíbe, sin que nada se rompa, porque el navegador ya está traduciendo por debajo.

⚠️
La ruta real no existe para ti

No hay ninguna API que te devuelva la ruta del sistema donde vive un archivo de OPFS, y no la habrá: exponerla filtraría el sistema operativo, el nombre de usuario y la estructura del perfil. Si tu diseño necesita esa ruta —para pasársela a otro proceso, para que el usuario la abra en su editor, para que un backup la recoja— OPFS no es la primitiva que buscas.

Un modelo de permisos por ausencia

La File System Access API arrastra toda la maquinaria que cabe esperar de algo que toca los archivos personales: exige un gesto del usuario, abre un diálogo del sistema, concede permisos que se consultan con queryPermission() y se piden con requestPermission(), y esos permisos caducan al recargar la pestaña. Un manejador guardado en IndexedDB sobrevive a la recarga, pero vuelve sin permiso y hay que revalidarlo.

OPFS no tiene nada de eso. No hay diálogo, no hay gesto, no hay requestPermission(), y funciona igual desde el hilo principal, desde un Worker o desde un Service Worker, en el primer milisegundo de vida de la página.

// Detección de capacidad, antes de comprometer la arquitectura.
const hayOpfs = typeof navigator !== 'undefined'
  && navigator.storage
  && typeof navigator.storage.getDirectory === 'function';

if (!hayOpfs) {
  // Camino alternativo sobre IndexedDB, o degradar a solo memoria.
}

La pregunta razonable es por qué se considera aceptable que un origen escriba en el disco sin avisar. La respuesta es que el permiso protege un recurso ajeno, y aquí no hay ninguno: el sitio no puede leer nada que no haya escrito él mismo, ni enumerar lo de otros, ni siquiera comprobar si otro origen existe. Lo único que sí pertenece al usuario es el espacio libre del disco, y ese recurso no se gobierna con un diálogo —que nadie sabría contestar— sino con la cuota y el desalojo, que es el tema de la lección 5.

🚪

Origen del contenido

Con el selector accedes a archivos que el usuario ya tenía y que otros programas conocen. En OPFS solo existe lo que tu propio código escribió: no puedes descubrir nada ajeno porque no hay nada ajeno.

🔕

Por qué no pide permiso

Un permiso protege un recurso del usuario. Aquí el recurso es tuyo, así que no hay nada que autorizar. Lo único que sí afecta al usuario es el espacio en disco, y eso se gobierna con la cuota, no con un diálogo.

⚙️

Dónde se puede usar

La API asíncrona está disponible en el hilo principal y en cualquier worker. El camino síncrono de alto rendimiento, en cambio, solo existe dentro de un worker dedicado: es el tema del nivel 10.

🧹

Quién puede borrarlo

El usuario, siempre: limpiar datos de navegación se lo lleva entero. Y el navegador, si aprieta el disco. Que sea invisible no lo hace intocable.

ℹ️
Estado del soporte

El árbol asíncrono que ves en este nivel está disponible en los tres motores. Desde finales de 2025 el soporte es razonable en escritorio y móvil, con las diferencias habituales en los detalles más nuevos de la especificación. Comprueba navigator.storage?.getDirectory antes de asumir nada y ten un camino alternativo sobre IndexedDB para el resto.

Qué habilita de verdad

Por primera vez el navegador te da una primitiva y no un producto

Todo lo que la plataforma web te había dado para guardar datos era un producto terminado con opiniones incorporadas: localStorage decide que los valores son cadenas y que las operaciones son síncronas y bloqueantes; IndexedDB decide que hay transacciones, que existe un modelo de índices y que los valores viajan por el algoritmo de clonado estructurado; Cache Storage decide que la clave es una petición HTTP. Cada una resuelve bien su caso y estorba en todos los demás, porque cuando la abstracción que te dan no es la que necesitas, tu única salida es emular la tuya encima y pagar dos veces. OPFS rompe ese patrón: no tiene opinión sobre tus datos. Un archivo es una secuencia de bytes con un tamaño y un offset, y eso es todo. Ese vacío deliberado es justo lo que convierte al navegador en un sitio donde puede correr un motor de base de datos de verdad, porque todos los motores de base de datos de los últimos cuarenta años —su registro de escritura anticipada, su caché de páginas, su árbol B, su protocolo de recuperación tras un fallo— están escritos contra exactamente esa primitiva y ninguna otra. Compilar SQLite a WebAssembly y darle OPFS como capa de archivos produce una base de datos de varios gigabytes que responde a velocidad casi nativa dentro de una pestaña, y eso no ocurre porque OPFS sea rápido: ocurre porque por fin no hay que traducir. La lección estructural es que la potencia de una plataforma no se mide por cuántas abstracciones de alto nivel te ofrece, sino por si te deja bajar cuando ninguna te sirve. Durante dos décadas el navegador no dejaba bajar. Ahora sí.

Este nivel recorre el camino asíncrono completo: el árbol de directorios en la lección 2, la lectura y la escritura en la 3, la justificación frente a IndexedDB en la 4 y los límites en la 5. El camino síncrono —los manejadores de acceso que hacen posible SQLite y que solo existen dentro de un worker— es el nivel 10 entero.

⚔️ Comprueba el aislamiento con tus manos
  1. Abre la consola en dos sitios cualesquiera, ejecuta await navigator.storage.getDirectory() en ambos y escribe un archivo distinto en cada uno. Lista después el árbol de cada sitio y confirma que ninguno ve al otro.
  2. Busca en las herramientas de desarrollo dónde se muestra ese archivo. Anota lo que encuentres y lo que no: esa carencia es material para la lección 5.
  3. Ejecuta await navigator.storage.estimate() antes y después de escribir diez megabytes y observa cómo se mueve el consumo.
  4. Repite la escritura desde un Worker sin ninguna interacción del usuario y verifica que funciona igual. Luego intenta lo mismo con showOpenFilePicker() y razona por qué falla.
  5. Escribe en una frase la diferencia entre las dos puertas sin usar la palabra permiso.