June 4, 2026

@funbrew/pdf-client: Browser SDK for PDF, tus Upload and SSE

sdkjavascripttypescriptnpmclient

Released on May 31, 2026, @funbrew/pdf-client is a browser-focused JavaScript SDK that brings three capabilities together in one package:

  • tus.io resumable uploads — automatic recovery after network interruptions
  • SSE real-time progress — job monitoring via EventSource
  • PDF generation and transformation — generate from HTML/URL/Markdown, plus compress, merge, extract text, and convert to images

@funbrew/pdf vs @funbrew/pdf-client: Which One to Use

Before diving in, here is the key distinction to avoid confusion with the existing Node.js SDK:

@funbrew/pdf @funbrew/pdf-client
Runtime Node.js (server-side) Browser (client-side)
Install npm install @funbrew/pdf CDN or ESM URL (npm publishing planned)
tus upload No Yes — resumable, parallel-capable
SSE progress No Yes — subscribeToJob()
Primary use case Next.js API routes, Lambda functions File drop UIs, in-browser form submissions
Type definitions Bundled with npm package CDN at /sdk/types/index.d.ts

Rule of thumb: server-side code uses @funbrew/pdf; browser code uses @funbrew/pdf-client.

Installation

CDN (script tag)

Drop this into any HTML page to get the UMD build:

<script src="https://pdf.funbrew.cloud/sdk/pdf-client.umd.js"></script>
<script>
  const client = new PdfClient.PdfClient({
    baseUrl: 'https://pdf.funbrew.cloud',
    apiKey: 'sk-...',
  });
</script>

ESM (bundler or native modules)

For Vite, webpack, Rollup, or any bundler that handles ESM imports:

import { PdfClient } from 'https://pdf.funbrew.cloud/sdk/pdf-client.esm.js';

const client = new PdfClient({
  baseUrl: 'https://pdf.funbrew.cloud',
  apiKey: 'sk-...',
});

TypeScript type definitions

Type definitions are served from the CDN. Download the .d.ts file locally for the smoothest IDE experience, or reference it directly in your tsconfig.json:

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@funbrew/pdf-client": ["https://pdf.funbrew.cloud/sdk/types/index.d.ts"]
    }
  }
}

Initialization

import { PdfClient } from 'https://pdf.funbrew.cloud/sdk/pdf-client.esm.js';

const client = new PdfClient({
  baseUrl: 'https://pdf.funbrew.cloud', // no trailing slash
  apiKey: 'sk-...',                      // sent as X-API-Key header
});

The full PdfClientOptions interface:

interface PdfClientOptions {
  baseUrl: string;       // e.g. 'https://pdf.funbrew.cloud'
  apiKey: string;        // your PDF API key
  fetch?: typeof fetch;  // optional: swap in a custom fetch (useful for tests)
}

Generating PDFs

The SDK wraps the HTML, URL, and Markdown generation endpoints:

// Generate from HTML
const result = await client.generateFromHtml({
  html: '<h1>Invoice</h1><p>Total: $1,200</p>',
  options: { page_size: 'A4' },
});
console.log(result.download_url); // https://pdf.funbrew.cloud/downloads/...

// Generate from URL
const fromUrl = await client.generateFromUrl({
  url: 'https://example.com/report',
  options: { page_size: 'A4', landscape: true },
});

// Generate from Markdown
const fromMd = await client.generateFromMarkdown({
  markdown: '# Monthly Report\n\nContent here...',
  options: { theme: 'github' },
});

The GenerateResult type:

interface GenerateResult {
  filename: string;
  download_url: string;
  file_size: number;
  expires_at?: string | null;
  job_id?: string;
}

Large File Uploads (uploadFile)

uploadFile() implements the tus.io protocol for resumable uploads. If the network drops, tus automatically retries with exponential backoff and resumes from where it left off — no data is re-sent unnecessarily.

const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];

const upload = await client.uploadFile(file, {
  chunkSize: 5 * 1024 * 1024, // 5 MB chunks (default)
  onProgress: (sent, total) => {
    const pct = Math.round((sent / total) * 100);
    document.querySelector('#progress').textContent = `${pct}%`;
  },
});

console.log(upload.key);  // UUID — pass as upload_id to merge/compress/etc.
console.log(upload.url);  // tus Location URL
console.log(upload.size); // file size in bytes

Full UploadOptions:

interface UploadOptions {
  chunkSize?: number;                // default: 5 MB
  retryDelays?: number[];            // default: [0, 1000, 3000, 5000, 10000]
  metadata?: Record<string, string>; // extra Upload-Metadata fields
  onProgress?: (sent: number, total: number) => void;
  signal?: AbortSignal;              // cancel the upload
  parallelUploads?: number;          // default: 1
}

Parallel uploads for high-latency networks

On mobile or satellite connections where round-trip latency is high, splitting the file into parallel chunks significantly improves throughput. The SDK uses the tus Concatenation extension to split the file client-side, upload chunks in parallel, and let the server reassemble them.

const upload = await client.uploadFile(file, {
  parallelUploads: 4, // split into 4 chunks, upload simultaneously
  onProgress: (sent, total) => console.log(`${sent}/${total}`),
});

Caveat: parallel uploads are only supported when the server storage is in local mode. Servers configured with s3-multipart storage return a 400 error.

Cancelling uploads

const controller = new AbortController();

const uploadPromise = client.uploadFile(file, {
  signal: controller.signal,
  onProgress: (sent, total) => console.log(`${sent}/${total}`),
});

document.querySelector('#cancel').addEventListener('click', () => {
  controller.abort();
});

try {
  const upload = await uploadPromise;
} catch (err) {
  if (err.name === 'AbortError') console.log('Upload cancelled');
}

Real-Time Progress (subscribeToJob)

Long-running operations — merging dozens of PDFs, converting many pages to images, compressing a large file — expose progress through Server-Sent Events. See the SSE progress streaming guide for a deeper look at how the event stream works.

const jobId = crypto.randomUUID(); // generate a UUIDv4

// Start listening before kicking off the job
const sub = client.subscribeToJob(jobId, {
  onProgress: ({ phase, progress, message }) => {
    console.log(phase, progress, message);
    // phase: 'queued' | 'preprocessing' | 'rendering' | 'uploading' | 'done' | 'failed'
  },
  onDone: ({ data }) => {
    console.log('Done!', data?.download_url);
    // sub closes automatically
  },
  onError: (err) => {
    console.warn('SSE error', err);
  },
});

// Pass the same jobId when making the request
await client.compress(
  { upload_id: upload.key, quality: 'medium' },
  { jobId },
);

// You can also close manually at any time
// sub.close();

Return value of subscribeToJob():

{
  close: () => void;    // manually close the SSE connection
  source: EventSource;  // the underlying EventSource instance
}

Job phase progression:

queued → preprocessing → rendering → uploading → done
                                               ↘ failed

Full Pipeline: Upload → Compress → Done Notification

Here is a self-contained example that wires upload, progress display, and download link together. For more on the tus upload side, see the resumable upload guide.

async function compressPdfWithProgress(file, quality = 'medium') {
  const jobId = crypto.randomUUID();

  const updateUI = (phase, pct) => {
    document.querySelector('#status').textContent = `${phase} ${pct}%`;
  };

  // 1. Subscribe to progress before kicking off the job
  const sub = client.subscribeToJob(jobId, {
    onProgress: ({ phase, progress }) => updateUI(phase, progress),
    onDone: ({ data }) => {
      updateUI('done', 100);
      const link = document.querySelector('#download');
      link.href = data.download_url;
      link.style.display = 'block';
    },
    onError: () => updateUI('error', 0),
  });

  try {
    // 2. Upload via tus
    updateUI('uploading', 0);
    const upload = await client.uploadFile(file, {
      onProgress: (sent, total) =>
        updateUI('uploading', Math.round((sent / total) * 100)),
    });

    // 3. Kick off the compress job with our jobId
    await client.compress(
      { upload_id: upload.key, quality },
      { jobId },
    );
  } catch (err) {
    sub.close();
    throw err;
  }
}

// Usage
const file = document.querySelector('#pdf-input').files[0];
await compressPdfWithProgress(file, 'high');

Using an Uploaded File with Other Operations

upload.key acts as an upload_id that you can pass to any transformation endpoint. Note that upload_id (for tus-uploaded files) and filename (for server-stored files) are mutually exclusive — use one or the other.

const upload = await client.uploadFile(file);

// Merge multiple PDFs
const merged = await client.merge({
  upload_ids: [upload.key, secondUpload.key],
});

// Extract text
const extracted = await client.extractText({
  upload_id: upload.key,
});

// Convert pages to images
const images = await client.toImage({
  upload_id: upload.key,
  format: 'png',
  dpi: 150,
});

Error Handling

API errors are thrown as standard Error objects with two extra properties: status (HTTP status code) and response (the parsed response body).

try {
  const result = await client.compress({
    upload_id: 'nonexistent-id',
    quality: 'medium',
  });
} catch (err) {
  console.error(err.message);   // e.g. "HTTP 404"
  console.error(err.status);    // 404
  console.error(err.response);  // { error: 'Not found', ... }
}

Upload retry configuration

uploadFile() retries automatically using the delay sequence [0, 1000, 3000, 5000, 10000] ms. Override this for more aggressive or more conservative retry behavior:

const upload = await client.uploadFile(file, {
  retryDelays: [0, 2000, 5000, 10000, 30000], // back off more slowly
});

TypeScript Support

The SDK is written in TypeScript and exports all key interfaces:

import type {
  PdfClientOptions,
  UploadResult,
  UploadOptions,
  GenerateResult,
  ProgressEventPayload,
  SubscribeOptions,
} from 'https://pdf.funbrew.cloud/sdk/types/index.d.ts';

// ProgressEventPayload structure
interface ProgressEventPayload {
  phase: string;           // 'queued' | 'preprocessing' | 'rendering' | 'uploading' | 'done' | 'failed'
  progress: number;        // 0–100
  message?: string | null;
  data?: Record<string, unknown>;
  occurred_at: string;     // ISO 8601
}

With proper types in place, onProgress callbacks and result.download_url accesses get full IDE autocompletion and type checking.

Security Considerations

Because @funbrew/pdf-client runs in the browser, the API key is visible to end users. For production deployments, consider one of these patterns:

  • Scoped keys: Issue API keys that only allow specific operations (e.g., upload + compress only). Configure permissions in the dashboard.
  • Short-lived tokens: Have your backend issue a temporary operation token that expires after a few minutes. The frontend uses the token; the real API key never leaves the server.
  • Backend proxy: Route requests through your own API (browser → your server → FUNBREW PDF). This keeps the API key fully server-side.

Live Demo

The SDK Playground (/sdk-playground) lets you try the full flow in a browser: drop a file, watch the tus upload progress, trigger compression with SSE progress updates, then download the result — all from one page.

The Playground is useful for experimenting with HTML templates before integrating them via the SDK.

Summary

@funbrew/pdf-client consolidates three distinct capabilities into a single, ergonomic browser SDK:

Feature Method / Detail
Resumable upload uploadFile() — tus.io protocol, auto-retry, parallel-capable
SSE progress subscribeToJob() — EventSource-based, phase-aware
PDF generation + ops generateFromHtml(), compress(), merge(), and more

For server-side TypeScript patterns using the Node.js SDK, see the TypeScript PDF guide. For a deep dive on SSE mechanics, read the SSE progress streaming guide. For everything about resumable uploads, visit the tus resumable upload guide.

Browse use cases to see how other teams are integrating PDF generation into their products.

Powered by FUNBREW PDF