DEV Community

Cover image for Flutter Secure QR Medical Profile Sharing
Muhammad Uzair
Muhammad Uzair

Posted on Originally published at uzairxdev.online

Flutter Secure QR Medical Profile Sharing

In an emergency, first responders need facts in seconds: allergies, blood group, chronic conditions, emergency contacts. The problem is that the fastest way to share those facts — a QR code anyone can scan — is also the least private way to store them. That tension shaped MedTagID, an app that stores emergency medical details and shares them instantly through a secure QR code, with encrypted storage, a simple dashboard for updating the profile, and Firebase Auth plus Firestore behind it.

This guide covers the Flutter pattern that makes that design work: a QR code that encodes a reference instead of a record, encryption before anything touches the cloud, and Firestore rules that keep the profile owned by exactly one person.

The one rule: the QR code is a pointer, not a record

A QR code is not a secure container. Any phone camera can decode it, any screenshot preserves it, and a printed code cannot be recalled once it leaves your hands. So the code must never contain allergies, medications, or conditions. It contains one thing: a short, opaque profile reference such as a document ID.

class EmergencyProfile {
  const EmergencyProfile({
    required this.id,
    required this.displayName,
    required this.bloodGroup,
    required this.allergies,
    required this.conditions,
    required this.medications,
    required this.emergencyContacts,
  });

  final String id;
  final String displayName;
  final String bloodGroup;
  final List<String> allergies;
  final List<String> conditions;
  final List<String> medications;
  final List<String> emergencyContacts;

  /// Only the fields the user explicitly marked as shareable.
  Map<String, dynamic> toEmergencyView() => {
        'displayName': displayName,
        'bloodGroup': bloodGroup,
        'allergies': allergies,
        'conditions': conditions,
        'emergencyContacts': emergencyContacts,
      };
}
Enter fullscreen mode Exit fullscreen mode

Notice what is missing from toEmergencyView: the full medication list and anything else the user did not choose to share. The QR code points at the profile; the app decides what a responder sees. If the user later changes their mind about a field, the change applies everywhere at once because the code never carried the data in the first place.

Encrypt on the device, before Firestore

Encryption that happens on the server protects data in transit. Encryption that happens on the device protects it in storage too: a leaked export, a misconfigured bucket, or an over-broad rule cannot hand over readable medical details. The pattern is simple — encrypt sensitive fields with AES before the write, and decrypt only when rendering.

import 'package:encrypt/encrypt.dart' as enc;

class ProfileCrypto {
  ProfileCrypto(String base64Key)
      : _key = enc.Key.fromBase64(base64Key),
        _iv = enc.IV.fromLength(16);

  final enc.Key _key;
  final enc.IV _iv;

  String encryptText(String plain) {
    final encrypter = enc.Encrypter(enc.AES(_key));
    return encrypter.encrypt(plain, iv: _iv).base64;
  }

  String decryptText(String cipher) {
    final encrypter = enc.Encrypter(enc.AES(_key));
    return encrypter.decrypt64(cipher, iv: _iv);
  }
}
Enter fullscreen mode Exit fullscreen mode

Two rules matter more than the cipher itself. First, the key must live in secure storage on the device — Flutter Secure Storage backed by the platform keystore — never in Firestore, never in source control, and never inside the QR code. Second, encrypt field by field rather than the whole document, so the app can still query by owner ID and updated timestamp without decrypting everything. The document ID and ownership fields stay plaintext; the medical content does not.

A related lesson from the health side of this work: a vitals app only works if logging takes seconds, which is why the entry flow in Build a Health Vitals Tracker in Flutter is built around one-handed, sub-ten-second input. The same discipline applies here — the dashboard for editing a medical profile must be fast enough that people actually keep it up to date, because a stale emergency profile is worse than no profile at all.

Generate the QR code with qr_flutter

With the reference in hand, rendering the code is the easy part. The qr_flutter package draws a QrImageView from any string, and the string here is just the profile reference.

import 'package:qr_flutter/qr_flutter.dart';

class EmergencyQrCard extends StatelessWidget {
  const EmergencyQrCard({super.key, required this.profileId});

  final String profileId;

  @override
  Widget build(BuildContext context) {
    // The QR payload is a reference only. No medical data is encoded.
    final payload = 'medtag://profile/' + profileId;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            QrImageView(
              data: payload,
              version: QrVersions.auto,
              size: 220,
              errorCorrectionLevel: QrErrorCorrectLevel.M,
            ),
            const SizedBox(height: 12),
            const Text(
              'Show this code to a first responder.',
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Error correction level M is a deliberate choice: it keeps the code dense enough to scan from a phone screen while tolerating the smudges and glare that printed cards collect. Regenerate this widget whenever the profile changes — the payload stays the same reference, but re-rendering on every profile save guarantees the displayed code and the stored profile never drift apart. If a printed card is lost, the user rotates the reference: generate a new profile ID, re-point the record, and the old card becomes useless.

Lock Firestore down to the owner

Encryption is the second line of defence; security rules are the first. Only the authenticated owner writes the profile, and only the owner reads the full document. The responder view is a separate, narrow document that contains only the emergency subset.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /profiles/{profileId} {
      allow read, write: if request.auth != null
        && request.auth.uid == resource.data.ownerId;
    }
    match /emergencyViews/{profileId} {
      // Narrow, user-approved subset only. No full record here.
      allow read: if true;
      allow write: if request.auth != null
        && request.auth.uid == resource.data.ownerId;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This split is the whole architecture in one place. The full encrypted profile is owner-only. The emergency view is readable by design — that is its job — but it contains only what the user approved, and nothing in it can be used to reach the full record. If you are setting up authentication for the first time, the email, Google, and phone flows in Flutter Firebase Authentication: Email, Google and Phone cover the sign-in side of this setup.

The dashboard is a safety feature, not a settings page

MedTagID treats the profile dashboard as part of the safety story: an intuitive screen where the user updates medical details in minutes. Three behaviours make it work:

Show the last-updated date next to the QR code. If the date is months old, the user sees the problem before a responder does. A stale profile should feel uncomfortable to look at.

Confirm before every shareable-field change. Toggling a field into the emergency view is a privacy decision, not a form edit. A short confirmation dialog — naming exactly what responders will see — prevents accidental exposure.

Keep the QR screen reachable in two taps from launch. In a real emergency the owner may be the one showing the code, possibly stressed, possibly with one hand free. Deep navigation is a failure mode here, not an inconvenience.

Checklist before you ship a medical QR feature

  • The QR payload contains a reference only; scan it with a plain camera app and confirm no medical text appears.
  • Sensitive fields are encrypted on the device before any Firestore write, and the key lives in secure storage.
  • Firestore rules deny full-profile reads to everyone except the owner, and the responder document holds only the approved subset.
  • Updating the profile regenerates the displayed code, and rotating the reference invalidates lost printed cards.
  • The dashboard shows the last-updated date and asks for confirmation before a field becomes shareable.
  • Test the responder view on a device that has never signed in, with mobile data off and on, because the scan will happen in exactly those conditions.

Medical features carry a responsibility that ordinary features do not: the data is sensitive, the moment of use is stressful, and the cost of a leak or a stale record is real. The secure design is not complicated — reference, not record; encrypt before the cloud; owner-only full access; a narrow, user-approved emergency view. Build those four habits in and the QR code becomes what it should be: the fastest safe way to hand a stranger the facts that could save your life.

Get in touch and let's talk about your project.


Originally published at uzairxdev.online

Top comments (0)