Going Native with Electrobun: Building a Desktop PDF Signer
Why a desktop app in 2025?
I know, I know. Everything should be a web app. But hear me out.
PDF signing with hardware smart cards is one of those things that genuinely needs native access. You’re talking to a physical device through PKCS#11 — a C interface for cryptographic tokens. Browsers can’t do this. Web Crypto API doesn’t support external hardware tokens. You need to talk to a USB port.

So the question wasn’t whether to build a desktop app — it was which framework to use.
Why not Electron?
I’ve built Electron apps before. It works. It also ships an entire Chromium browser with every install. For a focused utility app — open a PDF, sign it, save it — that’s like renting a moving truck to go get coffee.
200MB+ of RAM at idle. A dependency tree that makes node_modules look modest. For what? A file picker and a signature button.

Enter Electrobun
Electrobun is a relatively new framework for building desktop apps with Bun. Instead of bundling Chromium, it uses the system’s native webview (WebKit on macOS). The result:
- Tiny bundles — no Chromium download
- Bun-native — use Bun’s FFI, fast startup, native modules
- TypeScript-first — the whole API is typed
It’s still early and honestly a bit rough around the edges. But for a Bun-heavy stack like mine, it felt like the natural choice.
The PKCS#11 adventure
PKCS#11 is the standard interface for talking to smart cards, HSMs, and other cryptographic hardware. It’s a C API from the ’90s. And it shows.

The flow for signing a PDF with a smart card:
- Load the PKCS#11 shared library (
.dylib/.so/.dll) - Initialize the library and find the token slot
- Open a session and authenticate with the user’s PIN
- Find the signing key on the card
- Hash the PDF content
- Sign the hash using the private key on the card
- Embed the signature into the PDF’s signature field
Steps 1-6 happen through Bun’s FFI (Foreign Function Interface), calling into the native PKCS#11 library. Step 7 requires understanding the PDF specification’s signature structure — incremental saves, byte ranges, and PKCS#7 signature containers.
It’s not glamorous work. But it’s the kind of problem where a desktop app with native access genuinely shines. No server round-trips, no uploading sensitive documents to the cloud, no key material leaving the smart card.
The stack
- Runtime: Bun
- Desktop framework: Electrobun
- UI: React with TypeScript
- Crypto: PKCS#11 via Bun FFI
- PDF: Native PDF parsing and signature embedding
Nothing too wild individually. The interesting part is how they all fit together in a desktop context.
Lessons from the trenches
Test on other platforms early
Electrobun’s macOS experience is the most polished. If you need cross-platform, test early and often. Don’t ask me how I learned this.

Abstract the PKCS#11 layer aggressively
I started with direct FFI calls scattered everywhere and refactored toward a cleaner abstraction as the complexity grew. Should have started there. Direct C interop calls throughout your React app is… not great for readability.
Consider Tauri as an alternative
Tauri is more mature and has a bigger community. I went with Electrobun because I wanted the full Bun experience, but Tauri would have been a safer choice for a production app. If you’re evaluating both, try Tauri first and switch to Electrobun only if you specifically need Bun’s runtime.
Would I use Electrobun again?
For the right use case — yes. A focused desktop app where you need native access and you’re already in the Bun ecosystem? It’s a solid fit. For a complex cross-platform app with lots of native integrations? Probably go Tauri or even Electron (I know, I know).
Just PDF is one of those projects where the tech stack was driven entirely by the requirements: native hardware access, small footprint, and TypeScript-first DX. Electrobun checked all the boxes.
