PoC: scan an attendee's badge QR code, render a name tag with Typst, print it to a Bluetooth thermal/label printer — from a mobile browser, no native app. Built for AI Tinkerers.
Never use node, npm, or npx. Always bun --bun / bunx --bun.
Dev (two processes — the frontend proxies /api to the backend):
bun --bun server.ts # backend, port 3000
bun --bun dev # frontend, port 5173Production (one process):
bun --bun run build
bun --bun server.tsOpen on Android Chrome or desktop Chrome/Edge for Bluetooth printing to work — see Browser support below.
bun testRuns the regression suite (attendee parsing, the tag data shaper, the printer byte encoders) with Bun's built-in test runner. See Tests in AGENTS.md for what's covered and why.
The backend runs in mock mode by default: six fixture attendees,
no network calls, safe for a demo with no live data. Set
AIT_AGENT_API_KEY (a key from
aitinkerers.org/developers/api-keys)
to switch to live mode, which calls the real AI Tinkerers Agent
API. Check current mode at /api/checkin/mode.
cp .env.example .envThen fill in what you need — everything is optional, defaults are
demo-safe. .env is gitignored.
| Variable | Default | Purpose |
|---|---|---|
AIT_AGENT_API_KEY |
unset (mock mode) | Bearer key. Get one from aitinkerers.org/developers/api-keys. Set to switch to live mode. |
PORT |
3000 |
Backend port. |
The tag layout lives in src/lib/typst/tag.typ,
a real, standalone Typst file, not a string built up in TypeScript.
The app compiles it with attendee data passed through Typst's own
sys.inputs mechanism, so the exact same file is also editable and
testable on its own with the Typst CLI,
independent of the web app:
cd src/lib/typst
cp sample-data.example.json sample-data.json # one-time, like .env.example -> .env
typst watch tag.typEdit sample-data.json or tag.typ and see the result update live.
sample-data.json is gitignored, it's a personal scratch fixture; the
running app never reads it, it always supplies real attendee data
itself. See the comment at the top of tag.typ for the full picture,
including testing one-off values without editing any file:
typst compile tag.typ --input data='{"name":"Ada Lovelace"}'.
The tag's fonts (Roboto Condensed, the "AI Tinkerers" wordmark) live as
.ttf files in public/fonts/, committed to the
repo since Typst's rendering engine has its own font database,
completely separate from the browser/CSS font stack — a <link> or
@font-face is invisible to it. The web app loads them itself
(src/lib/typst/render.ts); when testing tag.typ standalone with
the CLI, point it at the same files: typst watch tag.typ --font-path ../../../public/fonts.
- Scan — camera QR scan, looks up the attendee, shows an editable company field and a live Typst-rendered tag preview, connects to a BLE printer and prints.
- Test badges — QR codes for the six mock attendees, in the same
URL shape real AIT badges use
(
https://{city}.aitinkerers.org/meetup/confirm_attendance/{token}/). - Printer Lab — connect to any BLE printer directly, list its services/characteristics, send raw TSPL or ESC/POS commands. Run this against new hardware before trusting the main print flow.
Not locked to one printer. src/lib/printer/known-services.ts lists
known BLE UART service UUIDs shared across most cheap thermal/label
printers (Munbyn, Phomemo, Niimbot, generic ESC/POS printers). To add
a new printer: find its service UUID with a BLE scanner app (e.g. nRF
Connect), add it to the registry, done — no other code changes needed.
See PRINTERS.md for the full picture: which
printers are confirmed working, how the transport/protocol
compatibility model works, and the step-by-step process for testing
and adding a new printer. Point the AIT Global team there.
Web Bluetooth: Chrome/Edge on desktop (Windows/Mac/Linux) and Android only. Not supported on iOS, in any browser — Apple requires all iOS browsers to use WebKit, which has never implemented Web Bluetooth.