vCard QR Generator
A small self-hosted web app that turns contact details into vCard QR codes for business cards, with an optional logo in the center. I built it to run as one local process, and because the contact lives inside the QR itself, any phone camera can scan it and offer to save the contact with no internet needed.
Architecture
One FastAPI process serves server-rendered Jinja2 pages: a dashboard, an employee form, and a settings page. Employees are rows in SQLite through SQLAlchemy, and the vCard text is not stored but rebuilt from the row whenever a QR or a .vcf download needs it. On each save, segno encodes that text into a PNG and an SVG named after the employee's slug, and Pillow pastes the logo onto the PNG. Uploading a new company logo regenerates every QR that uses the default.
Key decisions
A vCard builder written by hand
Most vCard libraries produce output that differs slightly from what Apple Contacts and Google Contacts expect, so I wrote a small builder by hand. It emits vCard 3.0 with CRLF line endings and puts N before FN, because iOS is picky about field order. Names, job title, and company are escaped for backslashes, commas, semicolons, and newlines, with backslashes done first so the new escapes are not doubled.
Level H error correction to make room for a logo
The PNG gets a square logo pasted over its center, 22% of the image width, which covers part of the code. I generate every code at level H, the highest error correction, which can recover about 30% of the data, so it still scans on modern phones. The SVG skips the logo and stays a clean vector for print, where a designer can place the logo by hand.
One slug per person names every file
Each employee gets a slug from their name: accents are stripped, the text is lowercased, and spaces become hyphens. If the slug is taken, the app adds -2, -3, and so on. The slug names that person's PNG, SVG, and custom logo, so when a rename changes it, the app deletes the old QR files, moves the logo, and writes new ones.
The hard part
The photo phones would not save
Earlier versions put a base64 photo in the vCard, but phones usually did not save it. The camera apps on iOS and Android read only the common fields from a QR, while the Contacts app's full parser does read the photo when a .vcf file is opened. The only reliable fix is a QR that points to a server, which needs a public host and internet at scan time. I kept the QR static and offline, removed the photo, and moved the branding to a logo in the center of the code.
Security and performance
- The launch scripts bind the app to 127.0.0.1 only and there is no login, so it stays a local tool for one user. The README says not to expose it publicly without adding authentication and HTTPS.
- A logo upload is rejected if it is empty, larger than 5 MB, declared as anything but PNG or JPG, or cannot be decoded by Pillow. The app then saves a re-encoded 400 by 400 pixel PNG, never the original file.