Daniel Kolawole AinaTL;DR: I built a QR-based medical ID that works in 20 languages without storing data on servers....
TL;DR: I built a QR-based medical ID that works in 20 languages without storing data on servers. Medical profiles live in the URL fragment. Translation powered by Lingo.dev.Live demo | GitHubStack: FastAPI, Vanilla JS, Tailwind, Lingo.dev, Web Speech API---## The ProblemLanguage barriers kill people in emergency rooms. Your medical bracelet says you're allergic to penicillin—but it's in English. The doctor doesn't speak English.Existing solutions suck:- Medical bracelets: single language only- Translation apps: require both parties conscious- Hospital records: locked away, not portable---## The Architecture DecisionMost medical apps do this:
User creates profile → Store in database → Generate QR with ID
Responder scans → Server lookup → Display data
text
Problems:- Database breach = all records exposed- Requires authentication- Network dependency*My approach:*
User creates profile → Encode as Base64 → Stuff in URL fragment
Responder scans → JS decodes locally → Display
text
#) never reach the server.
javascriptconst profile = { name: "John Doe", allergies: ["Penicillin"], conditions: ["Asthma"], medications: ["Albuterol"]};const encoded = btoa(JSON.stringify(profile));// Result: https://pulsecard.onrender.com/#card/eyJuYW1lIjoiSm9obiBEb2UifQ
Benefits:
✅ No database needed
✅ Works offline after first load
✅ Zero breach surface
✅ Infinitely scalable (just URLs)
Tradeoffs:
❌ Anyone with link can view (security by obscurity)
❌ ~3KB practical payload limit
❌ No built-in expiration
The Translation Layer
Supporting 20 languages meant solving two problems:
1. Static UI Translation
Predefined strings (buttons, labels, medical terms) → translate ahead of time.
I used Lingo.dev's CLI:
JSON
// i18n/en.json (source file){ "medical": { "allergies": "Allergies", "bloodType": "Blood Type" }, "communicate": { "areYouInPain": "Are you in pain?" }}
Bash
# One command generates 19 language filesnpx lingo.dev@latest i18n
Result: i18n/es.json, i18n/ja.json, i18n/ar.json, etc.
140 strings × 20 languages = 2,800 translations automatically.
2. Dynamic User Content
User notes like "EpiPen in right jacket pocket" can't be pre-translated.
Solution: Real-time API translation via Lingo.dev:
Python
# FastAPI endpoint@app.post("/api/translate")async def translate_text(request: Request): data = await request.json() async with httpx.AsyncClient() as client: response = await client.post( "https://api.lingo.dev/v1/translate", headers={"Authorization": f"Bearer {LINGO_API_KEY}"}, json={ "text": data["text"], "source_locale": "en", "target_locale": data["target_locale"] } ) return response.json()
Performance:
Static translations: ~10ms (cached)
Dynamic translations: ~800ms (API call)
Total load time: < 1 second
The Flag Grid UX
Problem: When a German paramedic scans a QR from an unconscious Chinese patient, what language is the UI in?
Solution: Visual-only language picker.
JavaScript
const languages = [ { code: 'ja', flag: '🇯🇵', name: '日本語' }, { code: 'ar', flag: '🇸🇦', name: 'العربية' }, { code: 'zh', flag: '🇨🇳', name: '中文' } // ... 17 more];function renderLanguageSelector() { return ` <div class="grid grid-cols-4 gap-4"> ${languages.map(lang => ` <button onclick="selectLanguage('${lang.code}')"> <span class="text-5xl">${lang.flag}</span> <span>${lang.name}</span> </button> `).join('')} </div> `;}
Key design: Flags first, native script for names. Someone who speaks zero English can still navigate.
Voice Synthesis (And Its Limits)
For non-verbal patients, I added voice-enabled communication using Web Speech API:
JavaScript
function speakText(text, locale) { const voices = window.speechSynthesis.getVoices(); const voice = voices.find(v => v.lang.startsWith(locale)); if (!voice) { // Fallback: show text full-screen showTextFullscreen(text); return; } const utterance = new SpeechSynthesisUtterance(text); utterance.voice = voice; utterance.rate = 0.9; window.speechSynthesis.speak(utterance);}
Real-world discovery: Voice support is device-dependent, not just browser-dependent.
LanguageiOS SafariAndroid ChromeEnglish✅✅Japanese✅⚠️ SometimesArabic✅❌ Often missing
Solution: Always display text visually as backup.
Deployment Gotcha: Local vs Production URLs
During development, QR codes encoded:
text
http://localhost:8000/#card/eyJuYW1l...
This works on my machine. Scan from phone? Worthless.
Fix:
JavaScript
// WRONG: hardcodedconst url = `https://pulsecard.onrender.com/#card/${encoded}`;// RIGHT: deployment-awareconst url = `${window.location.origin}/#card/${encoded}`;
Now it automatically uses the correct domain.
The Debugging Disasters
Bug 1: GitHub's 100MB Limit
text
remote: error: File is 147MB; exceeds GitHub's 100MB limit
I'd committed node_modules/ before .gitignore.
Even after deleting the folder, the file was still in git history.
Nuclear fix:
Bash
rm -rf .gitgit initgit add .git commit -m "Initial commit"git push --force
Lost all commit history. But it worked.
Lesson: Add .gitignore FIRST.
Bug 2: The Blank Screen
Added a feature. Entire app went white.
The culprit:
JavaScript
// WRONG - executes immediately`<button onclick="${speakText(text)}">Click</button>`// RIGHT - executes on click`<button onclick="speakText('${text}')">Click</button>`
Template literals + inline handlers = footgun.
Bug 3: Missing Comma
Added new JSON keys. Server returned 500.
JSON
{ "setup": { "createCard": "Create Card" "pinProtection": "PIN Protection" // Missing comma }}
Took 2 hours to find. JSON validators are your friend.
Performance Stats
Bundle size:
HTML: 12 KB
JS: 18 KB (no frameworks!)
CSS: 8 KB (Tailwind purged)
Total: 38 KB
Average website: 2.1 MB. PulseCard loads 55× faster.
Load times:
First visit: ~800ms
Cached: ~50ms
QR generation: ~120ms
Security Considerations
What's secure:
✅ No server-side storage
✅ HTTPS transport
✅ Optional AES-256 encryption
What's not:
❌ Link-based access (anyone with URL can view)
❌ No access logging
❌ No expiration
Future: PIN-based encryption using Web Crypto API:
JavaScript
async function encryptProfile(profile, pin) { const key = await crypto.subtle.deriveKey( { name: "PBKDF2", salt: new TextEncoder().encode("pulsecard"), iterations: 100000, hash: "SHA-256" }, await crypto.subtle.importKey( "raw", new TextEncoder().encode(pin), "PBKDF2", false, ["deriveKey"] ), { name: "AES-GCM", length: 256 }, false, ["encrypt"] ); // Encrypt profile...}
Tradeoff: Adds friction if patient is unconscious.
What I'd Build Next
Short term:
NFC tag support (Web NFC API)
Apple/Google Wallet integration
Offline PWA mode
Medium term:
Healthcare provider dashboard
EHR integration (Epic, Cerner)
Family linking (parent/child cards)
Long term:
Clinical validation
Emergency responder training mode
Regional medical terminology mapping
Try It
Live demo: pulsecard.onrender.com
Run locally:
Bash
git clone https://github.com/Onegreatlion/pulsecard.gitcd pulsecardpip install -r requirements.txtuvicorn main:app --reload
Tech Stack Summary
Backend: FastAPI (Python 3.9+)
Frontend: Vanilla JS, Tailwind CSS
Translation: Lingo.dev CLI + API
Voice: Web Speech API
QR: python-qrcode
Deployment: Render
Total lines of code: ~800
Development time: 48 hours
Bugs encountered: 47
Coffee consumed: Too much
Questions for the Community
Should I add PIN encryption even if it adds emergency friction?
How do I handle medical liability for translation errors?
What's the minimum medical data needed to save a life?
Worth partnering with healthcare orgs for validation?
Drop your thoughts below—I respond to everyone.
GitHub: github.com/Onegreatlion/pulsecard
Demo: pulsecard.onrender.com
Built with FastAPI, Lingo.dev, and way too much debugging.
format this for me so i can post on devto smoothly