Offline files and durability

Store an attachment in OPFS

Write and read an origin-private attachment file without confusing browser-managed files with user-visible documents.

The origin private file system, OPFS for short, gives your origin its own private folder. The files in it are real files, but the user never sees them in Finder or Explorer. The browser owns the folder, and only your origin can open it.

That makes OPFS the right place for the attachments in our field-notes app. A photo of a trail sign is file bytes, not a record. IndexedDB can hold a Blob, but OPFS is built for files and handles large ones better.

Write a file

The steps read like a normal filesystem. Get the root directory, get or create a file handle, open a writable stream, write, close:

const root = await navigator.storage.getDirectory()
const file = await root.getFileHandle('trail.txt', { create: true })
const writable = await file.createWritable()
await writable.write('North trail closed after rain')
await writable.close()

const saved = await file.getFile()
console.log(await saved.text())

The last line prints North trail closed after rain. getFile() returns a standard File object, so everything you know about Blob works: .text(), .arrayBuffer(), .size, URL.createObjectURL() for showing an image.

Notice writable.close(). The bytes aren’t guaranteed to be in the file until the stream closes. Forget it and a reload can show you an empty or partial file. DevTools has no built-in browser for OPFS, so list the root directory with root.entries() in the console to see what’s there.

For a real photo, replace the string with the File from an <input type="file">. write() accepts strings, Blobs, ArrayBuffers, and typed arrays.

Same quota, same lifetime

OPFS files count against the origin’s storage quota, the one we measured in the last lesson. They also vanish when the user clears site data, exactly like IndexedDB. It’s not safer than the database. It’s the same bucket, shaped like files.

Split metadata and bytes

The pattern: IndexedDB holds the note and a list of attachment IDs. OPFS holds the bytes, named by those IDs.

const note = {
  id: crypto.randomUUID(),
  title: 'Trail conditions',
  attachments: ['a1c9e3-trail-sign.jpg'],
}

The note is searchable through the index we built, and the file is one getFileHandle('a1c9e3-trail-sign.jpg') away. Don’t store the file handle object in the note. Handles are live objects tied to the current session, not stable business data. The filename is.

No permission prompt, and why

Opening a user’s Documents folder needs the File System Access API and a permission prompt. OPFS needs neither, because the files never leave the origin’s sandbox. That’s convenient. It also means OPFS is not how you give the user a file.

When the user expects a file they can open elsewhere, use a download link or showSaveFilePicker() where available. OPFS is for files the app needs, not files the user manages.

Write one text attachment, reference its filename from a note in IndexedDB, and reload. Read the note, then read the file through its name and print the text. Finally clear site data and confirm the database and the file are gone together.

Lesson completed