DEV Community

FlowPick Team
FlowPick Team

Posted on AI-assisted

Passing Large Files Between Browser Tools Without Uploading Them

A video finishes downloading. The next step is to trim it, extract its audio, or generate a transcript.

Making the user save the file, open another page, and select that same file again works. It also turns a connected workflow into a series of file-picker dialogs.

In FlowPick, I wanted the next tool to open with the result already available.

I’m the author of FlowPick, a browser extension and collection of audio and video tools. This article covers the file handoff implementation in its web tools: OPFS for media storage, IndexedDB for handoff records, and leases to coordinate file lifetime across tools and tabs.

The interesting part is not writing a file. It is deciding when that file can safely be deleted.

Separate the media from the handoff

FlowPick keeps two different things:

  • The media bytes, stored in the Origin Private File System.
  • A small handoff record, stored in IndexedDB.

The destination URL contains a randomly generated handoff ID:

/tools/video-trimmer?handoff=<random-id>
Enter fullscreen mode Exit fullscreen mode

It does not contain the media, an encoded file, or a filesystem path.

The destination uses that ID to look up the record and find the stored asset.

FlowPick file handoff: the source tool stores or retains an OPFS asset and creates an IndexedDB record. The destination receives a handoff ID, validates the record, and acquires its own asset lease.

The Origin Private File System, or OPFS, is browser-managed storage associated with an origin. It is separate from the user-visible files selected through a directory picker.

This design assumes the tools share the same origin and can access the same storage partition. It is not a mechanism for passing files to an unrelated website or another device.

Two paths: copy a Blob or reuse an OPFS file

Some tools produce a Blob. Others already have a completed result in OPFS.

Those cases should not take the same path.

When the result is a Blob

createToolHandoff() creates a managed file in a handoff directory and streams the Blob into it:

const fileHandle = await directory.getFileHandle(
  storageFilename,
  { create: true }
)

const writable = await fileHandle.createWritable()

await blob.stream().pipeTo(writable)
Enter fullscreen mode Exit fullscreen mode

This avoids explicitly converting the complete result into another ArrayBuffer in application code.

It still writes a copy into OPFS. Streaming the write does not mean the Blob itself occupies no memory or that the browser performs no internal buffering.

When the result is already in OPFS

createToolHandoffFromOPFS() finds the existing file, registers a handoff lease, and writes a record pointing to that asset.

It does not stream the media into a second handoff file.

FlowPick records this distinction explicitly:

storageMode: 'copied' | 'shared'
Enter fullscreen mode Exit fullscreen mode

Several downloader and recording paths can use the shared mode.

I describe this as avoiding an additional media-file copy. Calling the whole workflow “zero-copy” would promise too much: reading the file, creating a File wrapper, decoding media, or running another transformation can still involve allocations and additional storage.

Keep the record useful but small

Here is a shortened version of the handoff record:

interface ToolHandoffRecord {
  id: string
  sourceTool: string
  targetTool: string

  filename: string
  mimeType: string
  size: number

  createdAt: number
  expiresAt: number

  storage?: {
    directoryName: string
    primaryFilename: string
    cleanupFilenames: string[]
  }

  managedAssetId?: string
  handoffLeaseId?: string
  storageMode?: 'copied' | 'shared'
}
Enter fullscreen mode Exit fullscreen mode

The display filename and the internal storage filename serve different purposes.

The user might see interview.mp4, while the handoff directory contains a generated name. The record preserves the display name and MIME type for the receiving tool.

The cleanup list also matters. A media job can own more than its final output: separate audio and video inputs, a merged file, and temporary files may belong to the same asset.

FlowPick’s asset manager combines cleanup filenames when the same asset is retained again. That gives cleanup a view of the asset’s related files rather than just the filename currently shown in the UI.

Register ownership before writing bytes

In the Blob path, FlowPick registers the managed asset and its first lease before creating and writing the file.

The sequence is:

Register asset and lease
        ↓
Write OPFS file
        ↓
Commit handoff record
Enter fullscreen mode Exit fullscreen mode

If writing or recording the handoff fails, the error path releases the lease and attempts to remove the file.

Registering first also gives later cleanup something to reconcile if the page disappears during the write.

There is no single transaction spanning IndexedDB and OPFS here. The implementation uses ordering, rollback attempts, and later garbage collection to handle incomplete work.

That distinction is worth keeping explicit. An IndexedDB transaction can atomically register an asset and its lease; it cannot make an OPFS write part of that same transaction.

Open the tab before awaiting storage work

Preparing the handoff can take time, especially when a Blob must be written.

Opening a new tab after that asynchronous work risks losing the original user activation and having the browser block the popup.

FlowPick opens a blank tab synchronously from the click handler:

const tab = window.open('about:blank', '_blank')

if (tab) {
  tab.opener = null
}
Enter fullscreen mode Exit fullscreen mode

After the handoff is ready, the source navigates that tab to the tool URL:

const destination = new URL(path, window.location.origin)

destination.searchParams.set('handoff', handoffId)

tab.location.replace(destination.toString())
Enter fullscreen mode Exit fullscreen mode

The file preparation remains asynchronous. The decision to open the tab stays attached to the user’s click.

Validate before loading

The receiving tool does not treat every handoff ID as usable.

loadToolHandoff() checks that the record exists, has not expired, and names the expected destination:

if (
  !record ||
  record.expiresAt <= Date.now() ||
  record.targetTool !== expectedTarget
) {
  throw new Error('handoff-not-found')
}
Enter fullscreen mode Exit fullscreen mode

It then opens the stored file and acquires a consumer lease.

The target check catches a handoff opened in the wrong tool. It is not an authorization boundary against arbitrary code running on the same origin. The random ID is a lookup key, not a replacement for the browser’s origin boundary or the application’s security controls.

If the metadata exists but the file is missing, FlowPick removes the stale record and returns a handoff-not-found error.

File lifetime needs more than a reference in one tab

Consider this sequence:

  1. A downloader owns a completed file.
  2. It creates a handoff to a trimmer.
  3. The trimmer starts reading.
  4. The downloader clears its result.

Deleting the file at step four would be premature.

FlowPick tracks asset usage with leases. Roles include source, handoff, preview, conversion, result, and download.

A handoff lease and a receiving tool’s consumer lease represent different uses. Releasing one should not imply that every other use has finished.

The current asset manager uses these timing values:

const LEASE_TTL = 10 * 60 * 1000
const HEARTBEAT_INTERVAL = 30 * 1000
Enter fullscreen mode Exit fullscreen mode

Active leases refresh their expiry through a heartbeat.

The handoff record has a separate one-hour validity window. These are different clocks: record validity tells the destination whether it may load a handoff; asset leases and locks help decide whether the stored files are still in use.

A one-hour record expiry is not a guarantee that its file will remain available through every crash, storage eviction, or browser shutdown.

Locks and expiry solve different problems

Where available, FlowPick uses the Web Locks API.

An active asset holder keeps a shared lock. Garbage collection attempts to acquire an exclusive lock without waiting. If another holder still has the asset locked, that cleanup attempt skips it.

This is coordination between cooperating parts of the application. A Web Lock does not independently prevent unrelated code from deleting an OPFS file.

Without Web Locks, the collector relies on unexpired lease records. A separate IndexedDB lease coordinates garbage collectors across tabs.

Some workflows also opt into retainUntilExpiry. Their leases can be parked without immediately deleting the stored record, providing a bounded reload grace period. This is an explicit option, not a property of every handoff.

The fallback behavior is useful, but it is not identical to live lock ownership. Suspended tabs, interrupted heartbeats, and browser storage policies still need to be considered.

A release message is a hint, not the source of truth

FlowPick also uses BroadcastChannel to notify a still-open source tab when a handoff is released:

channel.postMessage({
  type: 'release',
  id: handoffId
})
Enter fullscreen mode Exit fullscreen mode

The message contains no media bytes.

Why involve the source tab? A closing destination page may not finish asynchronous IndexedDB and OPFS cleanup. A source tab that remains open can help complete that work.

The channel is optional. Its construction is guarded because an exposed API can still fail under a particular browser policy.

There is no durable message queue or delivery acknowledgement here. File ownership is tracked through the asset-management logic, rather than inferred solely from receiving a broadcast.

Expiry does not run itself

FlowPick checks for expired handoffs when creating new ones and rejects expired records when loading them. The source-side release watcher also schedules a cleanup attempt.

The asset manager has a separate reconciliation pass for unregistered files in managed directories, with a grace period before removal.

These are cleanup opportunities, not a background service that keeps running after the browser closes.

Browser-managed storage can also be cleared or evicted. The relevant constraints are described in MDN’s storage quotas and eviction documentation.

OPFS is working storage in this design. A handoff is not a backup.

What I would check before relying on this workflow

This article describes the implementation. It does not establish a maximum supported file size, a storage-saving benchmark, or a complete browser compatibility matrix.

The scenarios I would exercise include:

  • Creating a handoff from a Blob and from an existing OPFS result.
  • Clearing the source result while the destination still holds a lease.
  • Opening an expired handoff or one intended for another tool.
  • Reloading or closing the destination during processing.
  • Missing files, failed writes, and denied storage access.
  • Concurrent consumers and cleanup attempts.
  • Operation without Web Locks or BroadcastChannel.

The visible feature is a button that opens another tool with the file ready.

Underneath it, the important contract is about ownership: which file the record refers to, who is still using it, and what evidence is sufficient to delete it.

For FlowPick, keeping those questions separate from the media-processing code makes the handoff easier to reason about—and makes “continue in another tool” more than a shortcut to another file picker.


Disclosure: I’m the author of FlowPick. This article was drafted with AI assistance based on the project’s OPFS handoff and asset-management implementation. It does not report new performance measurements or cross-browser test results.

Top comments (0)