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 | GitHub
Stack: FastAPI, Vanilla JS, Tailwind, Lingo.dev, Web Speech API
The language problem 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(Horror incoming).
Existing solutions suck:
- Medical bracelets: single language only
- Translation apps: require both parties conscious
- Hospital records: locked away, not portable
The Architecture Decision
Most medical apps do this:
User creates profile → Store in database → Generate QR with ID
Responder scans → Server lookup → Display data
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
Why URL Fragments?
The key insight: URL fragments (everything after # never reach the server)
const 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
-Just URLs that makes it scalabe
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:
// i18n/en.json (source file)
{
"medical": {
"allergies": "Allergies",
"bloodType": "Blood Type"
},
"communicate": {
"areYouInPain": "Are you in pain?"
}
}
# One command generates 19 language files
npx 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: Realtime API translation via Lingo.dev:
# 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.
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:
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
| Language | iOS Safari | Android Chrome |
|---|---|---|
| English | Yes | Yes |
| Japanese | Yes | Sometimes |
| Arabic | Yes | Often missing |
Solution: Always display text visually as backup
Deployment: Local vs Production URLs
During development, QR codes encoded:
http://localhost:8000/#card/eyJuYW1l...
This works on my pc. Scan from phone? Worthless
Fix:
// WRONG: hardcoded
const url = `https://pulsecard.onrender.com/#card/${encoded}`;
// RIGHT: deployment-aware
const url = `${window.location.origin}/#card/${encoded}`;
Now it automatically uses the correct domain.
The Debugging Disasters
Bug 1: GitHub's 100MB Limit
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:
rm -rf .git
git init
git 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:
// 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.
{
"setup": {
"createCard": "Create Card"
"pinProtection": "PIN Protection" // Missing comma
}
}
Took 2 hours to find, i almost gave up here
Security Considerations
What's secure:
✅ No server-side storage
✅ HTTPS transport
✅ Optional AES-256 encryption
What's not:
❌ No access logging(Exccept user added a pin)
❌ No expiration
What I'd Build Next
Short term:
-NFC tag support
-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
-What if the patient was unconscious and he added a pin
Try It
Live demo: https://pulsecard.onrender.com
*Run locally:
*
Bash
git clone https://github.com/Onegreatlion/pulsecard.git
cd pulsecard
pip install -r requirements.txt
uvicorn 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
Coffee consumed: Too much
Questions for the Community
-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: https://pulsecard.onrender.com
Built with FastAPI, Lingo.dev, and way too much debugging.
Top comments (0)