diff --git a/content/posts/tessera-privacy-first-card-wallet.md b/content/posts/tessera-privacy-first-card-wallet.md new file mode 100644 index 0000000..b6b6449 --- /dev/null +++ b/content/posts/tessera-privacy-first-card-wallet.md @@ -0,0 +1,100 @@ +--- +title: 'Tessera: A Privacy-First Card Wallet' +description: 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. +date: 2026-09-18 12:00:00 +0200 +category: Projects +tags: [projects, pwa, privacy, webdev] +coverImage: tessera-homepage.png +coverImageAlt: Screenshot of Tessera's homepage showing a list of saved loyalty cards with their barcodes. +--- + +## 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 opened card detail](tessera-active_card.png) + +![Screenshot of Tessera's form for adding a new card](tessera-new_card_form.png) + +Try it at [tessera.hhu.cz](https://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: + +```bash +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. + +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](https://github.com/HangerThem/tessera), MIT licensed. If something's broken or missing, PRs are welcome. + +Best,
+Frank diff --git a/public/posts/tessera-active_card.png b/public/posts/tessera-active_card.png new file mode 100644 index 0000000..2112c07 Binary files /dev/null and b/public/posts/tessera-active_card.png differ diff --git a/public/posts/tessera-homepage.png b/public/posts/tessera-homepage.png new file mode 100644 index 0000000..69ca6f3 Binary files /dev/null and b/public/posts/tessera-homepage.png differ diff --git a/public/posts/tessera-new_card_form.png b/public/posts/tessera-new_card_form.png new file mode 100644 index 0000000..ed44999 Binary files /dev/null and b/public/posts/tessera-new_card_form.png differ