@funbrew/pdf-client: Browser SDK for PDF, tus Upload and SSE
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.