blog.hangerthem.com/content/posts/tessera-privacy-first-card-wallet.md

5.6 KiB

title description date updated category tags coverImage coverImageAlt
Tessera: A Privacy-First Card Wallet A simple PWA for managing loyalty and membership cards. No account, no ads, no data leaving your device. A privacy-first approach to digital card storage. 2026-09-18 12:00:00 +0200 2026-09-18 12:30:00 +0200 Projects
projects
pwa
privacy
webdev
tessera-cover.png Tessera cover with basic infroamtion about the post.

The Problem

I have too many loyalty cards.

Coffee shops, grocery stores, pharmacies, the occasional bookstore that insists on a points program - all of them want a piece of plastic in your wallet or an app on your phone.

The apps are the worst part. Each one wants an account. Each one collects data. Each one sends push notifications you didn't ask for. And half of them are barely functional web wrappers with a 40 MB download size.

There are wallet apps that aggregate cards, but they all share the same problem: they're cloud-based, account-gated, and you're trusting some startup with the barcode data from every store you shop at. That felt unnecessary.

All I want is to scan a barcode once and show it at checkout. That's it.

So I built Tessera.

What It Does

Tessera lets you scan the barcode or QR code from any physical loyalty card and save it to a local digital wallet. When you're at checkout, open the card and the barcode is displayed for scanning.

That's the whole thing.

No account required. No server involved. All data lives in IndexedDB in your browser. Once installed as a PWA, it works fully offline.

Screenshot of Tessera's homepage showing a list of saved loyalty cards with their barcodes.

Screenshot of opened card detail

Screenshot of Tessera's form for adding a new card

Try it at tessera.hhu.cz, or self-host it yourself, it is a static build.

Stack Decisions

This is where it got a bit different from my usual work.

My default is Next.js, and for most things that's the right call. But Tessera has no backend, no API, and no server-side rendering. It is a pure client-side app. Reaching for Next.js would have meant using a framework for a problem that does not need one.

The first prototype was built with vanilla JS and a few libraries. That worked for a proof of concept, but I wanted a more structured approach for the final version. So I pivoted to just adding Vite, that allowed me to use TypeScript and a proper build system without the overhead of a full framework.

But as I continued to expand the app, I found myself rebuilding the same systems that some lightweight framework would have provided, probably better than I could have done on my own.

So I went with Preact + Vite instead. Preact because it's small and I didn't need React's full weight for something this contained. Vite because it's just faster and the build setup is nearly zero friction.

For barcode scanning I used ZXing. It handles most common barcode formats well and worked reliably on both desktop and mobile cameras. For rendering the barcode when showing the card at checkout, BWIP-JS covers essentially every format I'd realistically encounter.

Storage is handled by IDB-Keyval, a thin wrapper over IndexedDB. I didn't want to deal with the raw IndexedDB API and didn't need anything heavy. IDB-Keyval is minimal.

The full stack:

Component Implementation
Framework Preact + Vite
Language TypeScript
Barcode scanning ZXing
Barcode rendering BWIP-JS
Storage IDB-Keyval (IndexedDB)
Icons Lucide

Why a PWA

The PWA constraint shaped a lot of the decisions here.

No app store means no review process, no gatekeeping, no fee. You open the URL, install from the browser, and it behaves like a native app. That's the right distribution model for something this simple.

It also forces you to be honest about offline support. If your app is installed and behaves like a native app, it should work like one, including without a connection. Since everything is local anyway, offline support came essentially for free.

The one trade-off is camera access for scanning, which requires HTTPS and the browser's permission prompt. Not a problem in practice, but worth knowing if you're self-hosting.

Self-Hosting

Tessera is a static build, so self-hosting is straightforward:

git clone https://github.com/HangerThem/tessera
cd tessera
bun install
bun run build

Serve the dist/ folder with anything like Nginx, Caddy, or Coolify. I'm running it on my own infrastructure at tessera.hhu.cz the same way.

What's Next

Tessera is intentionally small. I don't want to turn it into a platform.

As of now it is designed mainly for mobile use, but it works on desktop too. In the next few days I will focus on making it more desktop-friendly, with a better layout for larger screens and keyboard shortcuts for power users. Even tho I find the usage of something like this on desktop a bit niche, I know some people will want to use it there.

A few things I might add: card categories or grouping, export/import functionality, simple notes, and icon customization so your cards are easier to tell apart at a glance.

What I won't add: accounts, sync, a backend, analytics, or anything that sends data off your device. That's the whole point.

The source is on GitHub, MIT licensed. If something's broken or missing, PRs are welcome.

Best,
Frank