upflowi
Engine reference

A file transfer engine that stays out of your UI.

upflowi orchestrates uploads — concurrency, chunking, multipart, retries, progress, pause and resume, cancellation, resumable persistence — for TypeScript and JavaScript, in the browser and in Node.js, without an opinion on what your interface looks like.

pnpm add @upflowi/core @upflowi/transport-fetch @upflowi/provider-s3
Read the quickstart
01

What it is, precisely

Headless
No file picker, no dashboard, no React components. The engine, and nothing you didn't ask for.
Provider-agnostic
S3, R2, a VPS, or an API you haven't written yet — through one StorageProvider interface.
Credential-free
The S3 and R2 providers run on presigned URLs your backend issues. The SDK never holds a key.
Resumable
Paired with an UploadStore, a crashed or reloaded transfer skips parts already completed.
Strictly typed
strict: true, no any on the public surface, typed events, typed errors you can instanceof against.
02

Against the usual paths

A single-cloud SDK is fast to start and permanent to leave — switching storage means rewriting the upload path. A full-service uploader ships a UI you didn't ask for and now have to theme, translate, and maintain. upflowi keeps the orchestration — queueing, chunking, retry, progress, resumable state — identical across providers, and asks nothing of your interface.

03

Four layers, one boundary

Headless and provider-agnostic aren't claims — they're a consequence of keeping these four concerns apart. Each layer only ever talks to the one below it.

Orchestration

uploader.ts · upload.ts · queue.ts · state-machine.ts

Decides what to upload, in what order, and tracks status.

knowsqueue order, retry count, upload statusneverHTTP mechanics, a specific provider's API shape

Scheduling

scheduler.ts

Decides how many operations run at once — global, per-file, per-chunk.

knowsconcurrency limitsneverwhat the operation actually does

Transport

transport-fetch · transport-xhr

Moves bytes — HTTP mechanics, progress events, headers, abort wiring.

knowshow to send a request and report progressnevermultipart, S3, or any provider concept

Provider

provider-s3 · provider-r2 · provider-http

Defines which operations exist on a backend and maps them onto transport calls.

knowscreate / uploadPart / complete / abort / resumeneverhow bytes physically move, or how many run at once

A new transport or provider never touches scheduler.ts or queue.ts — if it does, the abstraction leaked.

The same boundary covers access control: whether an upload ends up public or private is your backend's call when it signs the URL — upflowi has no ACL concept, and it never serves a file back.

04

Packages

A pnpm workspace. Install @upflowi/core plus one transport; add a provider only for multipart transfers.

@upflowi/coreEngine. Zero runtime dependencies.
@upflowi/transport-fetchFetch API. Browser and Node 18+.
@upflowi/transport-xhrXMLHttpRequest. Browser only — continuous progress.
@upflowi/provider-s3AWS S3 multipart, via presigned URLs.
@upflowi/provider-r2Cloudflare R2 multipart, via presigned URLs.
@upflowi/provider-httpYour own backend — JSON over HTTP.
@upflowi/store-memoryIn-memory UploadStore. Tests, short-lived scripts.
@upflowi/store-indexeddbBrowser UploadStore backed by IndexedDB.
05

Quickstart

Multipart upload to S3, driven entirely by presigned URLs your backend issues.

quickstart.ts
import { createUploader } from "@upflowi/core";
import { createFetchTransport } from "@upflowi/transport-fetch";
import { createS3Provider } from "@upflowi/provider-s3";

const uploader = createUploader({
  concurrency: 3,
  transport: createFetchTransport(),
  provider: createS3Provider({
    getPresignedUrl: (operation) => backendClient.getS3PresignedUrl(operation),
  }),
});

const upload = uploader.add({ source: mySource });
upload.on("progress", (progress) => console.log(`${progress.percent.toFixed(1)}%`));
uploader.start();
06

Every shape of upload

No provider, one request — a destination URL and a source.

simple.ts
import { createUploader } from "@upflowi/core";
import { createFetchTransport } from "@upflowi/transport-fetch";

const uploader = createUploader({ transport: createFetchTransport() });

const upload = uploader.add({
  source: {
    fileId: "avatar.png",
    size: file.size,
    read: async () => file, // Blob, ArrayBuffer, ArrayBufferView, or string
  },
  options: { url: "https://your-backend.example.com/uploads/avatar.png" },
});

upload.on("completed", ({ result }) => console.log("done:", result));
uploader.start();
07

Events and errors, typed

Every error extends UploadError — branch on it with instanceof instead of parsing strings.

Typed events

startedUpload
fileIdstring

Emitted by every Upload handle.

progressUpload
fileIdstring
loadedBytesnumber
totalBytesnumber
percentnumber

Emitted by every Upload handle.

pausedUpload
fileIdstring

Emitted by every Upload handle.

resumedUpload
fileIdstring

Emitted by every Upload handle.

retryUpload
fileIdstring
attemptnumber
errorUploadError

Emitted by every Upload handle.

completedUpload
fileIdstring
resultunknown

Emitted by every Upload handle.

failedUpload
fileIdstring
errorUploadError

Emitted by every Upload handle.

cancelledUpload
fileIdstring

Emitted by every Upload handle.

queuedUploader
fileIdstring

Emitted by the Uploader.

allCompletedUploader
completedCountnumber
failedCountnumber

Emitted by the Uploader.

Typed errors

UploadErrorextends Error
codestring
messagestring
causeunknown
retryableboolean
fileId?string
partNumber?number

The base every other error extends. Branch on any of these with instanceof.

NetworkErrorextends UploadError

No response was received at all.

HttpErrorextends UploadError
statusnumber

A non-2xx response came back.

AbortErrorextends UploadError

The AbortSignal fired. retryable is always false.

RetryExhaustedErrorextends UploadError
attemptsnumber

Every configured attempt failed. cause is the last underlying error.

UploadValidationErrorextends UploadError

Bad input, caught before any network call.

ProviderErrorextends UploadError
providerCodestring

S3 / R2 / your backend rejected the operation.