# Building PulseCard: A Privacy-First Emergency Medical ID with 20 Languages

# Building PulseCard: A Privacy-First Emergency Medical ID with 20 LanguagesDaniel Kolawole Aina

TL;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

Why URL Fragments?The key insight: URL fragments (everything after #) 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
Enter fullscreen mode Exit fullscreen mode