How to Build a Web App to Scan NFC Tags and Barcodes

You can build a single web app that scans NFC tags and barcodes together using the browser’s native NDEFReader API and the latest Dynamsoft Barcode Reader Bundle. The app reads the AES key from a nearby NFC tag, scans an encrypted QR code, and decrypts its content entirely in the browser — no server required.

A web app scanning NFC tags and barcodes

Near-field communication (NFC) is a set of communication protocols that enables communication between two electronic devices over a distance of 4 cm (11⁄2 in) or less.1 We can store data in an NFC tag in the NDEF format and then use a phone with NFC support to read it.

Another invention for storing and sharing information is the barcode. It uses lines and patterns to represent data and can be read with an optical scanner or a camera.

NFC has advantages over barcode like low-light functionality, better security and less possibility of being deactivated with a pen. Barcode has advantages over NFC like a longer scanning range, less cost, no surrounding interference and more supported devices.

We can use NFC tags and barcodes together for sharing data, like storing a password or private key in an NFC tag and using it to decrypt the encrypted content stored in a QR code or storing the same data in both for better data integrity and ease of retrieval.

Chrome for Android has added NFC capability since version 89.2 We can use the NDEFReader interface to read and write NFC tags in browsers. As for barcode scanning, we are going to use Dynamsoft Barcode Reader.

What You’ll Build

  • A single-page web app that scans NFC tags with the NDEFReader API and barcodes with Dynamsoft Capture Vision.
  • A live barcode scanner that reads 1D/2D codes from the camera and stops automatically when a code is found.
  • A “Write to NFC Tag” action for every barcode you decode.
  • An AES decrypter that uses the value read from an NFC tag to decrypt the content stored in a barcode.
  • An NFC simulation fallback so users on browsers without NFC support can still enter a key and follow the same workflow.

Key Takeaways

  • NFC tags are read in the browser with the NDEFReader API (available in Chrome for Android since version 89). Barcodes are read with the Capture Vision API (CaptureVisionRouter).
  • To decode a barcode, call cvr.capture(canvas, 'ReadBarcodes_Default') and read the barcode items from result.items — not the older BarcodeReader/barcodeText interface.
  • CaptureVisionRouter.capture() does not support concurrent calls, so a mutex guard prevents overlapping frames in the camera loop.
  • The app runs entirely in the browser; no server or native mobile app is needed.

Prerequisites

To follow this tutorial, you need a text editor, a browser (Chrome for Android if you want to scan real NFC tags), and a way to serve the folder over localhost or HTTPS. If your browser does not support NDEFReader, the app offers a simulation flow so you can still try the full workflow. Dynamsoft also requires a license — get a 30-day free trial license to use the barcode reader in your project.

Step 1: Set Up the HTML and Load the SDKs

Start with an HTML file that loads the Dynamsoft Barcode Reader Bundle and CryptoJS from a CDN, and provides the app container, the NFC and barcode buttons, and the result lists.

<!DOCTYPE html>
<html>
<head>
  <title>NFC and Barcode Scanner</title>
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
  <script src="https://cdn.jsdelivr.net/npm/dynamsoft-barcode-reader-bundle@11.6.2100/dist/dbr.bundle.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.js"></script>
</head>
<body>
  <div class="app">
    <h2>NFC and Barcode Scanner</h2>
    <div>
      <label>Status:</label>
      <span id="status"></span>
    </div>
    <div>
      <button id="NFC-btn" onclick="toggleNFCScanning();">Scan NFC Tags</button>
      <button id="NFC-simulate-btn" onclick="simulateNFCTag();" style="display:none;">Simulate NFC Tag</button>
      <button id="barcode-btn" onclick="scanBarcodes();">Scan Barcodes</button>
    </div>
    <div id="NFC-note" style="display:none; margin-bottom: 8px; font-size: 13px; color: #666;"></div>
    <div>
      NFC Results:
      <ol id="NFC-results"></ol>
    </div>
    <div>
      Barcode Results:
      <ol id="barcode-results"></ol>
    </div>
    <button onclick="decrypt();">Decrypt</button>
    <div id="decrypted"></div>
  </div>

  <div class="scanner" id="scanner">
    <video id="video" autoplay playsinline muted></video>
    <canvas id="overlay"></canvas>
    <button class="close-btn" onclick="stopBarcodeScan();">Close</button>
  </div>
</body>
</html>

The .scanner element hosts the live camera feed and an overlay canvas. Note that we no longer use the legacy dynamsoft-javascript-barcode and dynamsoft-camera-enhancer SDKs — the barcode bundle provides the current Capture Vision API on its own, and the camera is accessed with getUserMedia.

Step 2: Scan NFC Tags

Check that the browser supports NDEFReader. If not, disable the NFC button and enable a simulation flow so users who do not have an NFC-capable device (for example, on a desktop browser) can still enter a key and follow the same workflow:

checkIFNFCSupported();
function checkIFNFCSupported(){
  if (!("NDEFReader" in window)) {
    // NFC is not available (e.g. on a desktop browser). Enable a simulation
    // flow so users can still try the full NFC + barcode workflow.
    document.getElementById("NFC-btn").disabled = true;
    document.getElementById("NFC-simulate-btn").style.display = "inline-block";
    const note = document.getElementById("NFC-note");
    note.style.display = "block";
    note.innerText = 'NFC is not supported in this browser. Use "Simulate NFC Tag" to enter a key and follow the same workflow.';
  }
}

The simulation uses a prompt() to ask for the key, then builds a record that mimics an NDEF record so the rest of the flow (displaying the value and decrypting the barcode) reuses the same code path:

function simulateNFCTag(){
  const defaultValue = "dynamsoft";
  const value = prompt("Enter the key to store in the NFC tag (simulated):", defaultValue);
  if (value === null || value === "") {
    return;
  }
  const bytes = new TextEncoder().encode(value);
  NFCResults = [{
    data: new DataView(bytes.buffer)
  }];
  displayNFCResults();
  updateStatus("Simulated NFC tag: " + value);
}

Add a scanNFCTags function to start scanning NFC tags. The AbortController lets you stop the scan:

let ndef;
let abortController;
let NFCResults = [];
async function scanNFCTags(){
  if (!ndef) {
    abortController = new AbortController();
    abortController.signal.onabort = event => {
      // All NFC operations have been aborted.
      console.log(event);
    };
    ndef = new NDEFReader();
    ndef.onreadingerror = () => {
      console.log("Cannot read data from the NFC tag. Try another one?");
    };
    ndef.onreading = event => {
      console.log("NDEF message read.");
      console.log(event);
      NFCResults = [];
      NFCResults = NFCResults.concat(event.message.records);
      displayNFCResults();
    };
  }
  ndef.scan({ signal: abortController.signal }).then(() => {
    console.log("Scan started successfully.");
  }).catch(error => {
    console.log(`Error! Scan failed to start: ${error}.`);
  });
}

Define the toggleNFCScanning function to start and stop the scan, and displayNFCResults to render the records:

function toggleNFCScanning(){
  const btn = document.getElementById("NFC-btn");
  if (btn.innerText === "Scan NFC Tags") {
    btn.innerText = "Stop Scanning NFC Tags";
    scanNFCTags();
  }else{
    btn.innerText = "Scan NFC Tags";
    abortController.abort();
  }
}

function displayNFCResults(){
  const ol = document.getElementById("NFC-results");
  ol.innerHTML = "";
  for (let index = 0; index < NFCResults.length; index++) {
    const record = NFCResults[index];
    const buf = record.data.buffer;
    const str = new TextDecoder().decode(buf);
    console.log(str);
    const li = document.createElement("li");
    li.innerText = str;
    ol.appendChild(li);
  }
}

Step 3: Initialize the Barcode Reader

The Dynamsoft Barcode Reader Bundle exposes the Capture Vision API. Initialize the license, load the core WebAssembly module, and create a CaptureVisionRouter:

let cvr;
async function initBarcodeReader() {
  updateStatus('Initializing...');
  try {
    Dynamsoft.License.LicenseManager.initLicense('YOUR-LICENSE-KEY', true);
    await Dynamsoft.Core.CoreModule.loadWasm(['DBR']);
    cvr = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();
    updateStatus('Initialized');
  } catch (ex) {
    updateStatus('Failed to initialize: ' + (ex.message || ex));
  }
}

Call initBarcodeReader() when the page loads.

Step 4: Scan Barcodes from the Camera

When the Scan Barcodes button is clicked, open the camera with getUserMedia, attach the stream to the <video> element, and start a processing loop:

let stream;
let interval;
let processing = false;

async function scanBarcodes(){
  try {
    stream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 } } });
    const video = document.getElementById("video");
    video.srcObject = stream;
    document.getElementById("scanner").style.display = "block";
    startProcessingLoop();
  } catch (error) {
    alert(`Error! Scan failed to start: ${error}.`);
  }
}

function startProcessingLoop(){
  stopProcessingLoop();
  interval = setInterval(captureAndDecode,100); // read barcodes
}

function stopProcessingLoop(){
  if (interval) {
    clearInterval(interval);
    interval = undefined;
  }
  processing = false;
}

Each frame is drawn to a canvas and passed to cvr.capture(). Because CaptureVisionRouter.capture() does not support concurrent calls, wrap it in a mutex so frames do not overlap:

async function captureWithLock(canvas) {
  if (processing) {
    return null;
  }
  processing = true;
  try {
    return await cvr.capture(canvas, 'ReadBarcodes_Default');
  } finally {
    processing = false;
  }
}

async function captureAndDecode() {
  if (!cvr) {
    return;
  }
  const video = document.getElementById("video");
  if (video.readyState !== video.HAVE_ENOUGH_DATA) {
    return;
  }
  const canvas = document.createElement('canvas');
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;
  const ctx = canvas.getContext('2d');
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  const result = await captureWithLock(canvas);
  if (result && result.items && result.items.length > 0) {
    barcodeResults = result.items.filter(item => item.type === Dynamsoft.Core.EnumCapturedResultItemType.CRIT_BARCODE);
    if (barcodeResults.length > 0) {
      displayBarcodeResults();
      stopBarcodeScan();
    }
  }
}

The barcode items are rendered in a list. When NDEFReader is available, every item also gets a Write to NFC Tag button:

function displayBarcodeResults(){
  const ol = document.getElementById("barcode-results");
  ol.innerHTML = "";
  for (let index = 0; index < barcodeResults.length; index++) {
    const item = barcodeResults[index];
    const li = document.createElement("li");
    const container = document.createElement("div");
    const span = document.createElement("span");
    span.innerText = item.text;
    container.appendChild(span);
    if ("NDEFReader" in window) {
      const btn = document.createElement("button");
      btn.innerText = "Write to NFC Tag"
      btn.addEventListener('click',function(){
        writeNFCTag(item.text);
      });
      container.appendChild(btn);
    }
    li.appendChild(container);
    ol.appendChild(li);
  }
}

Step 5: Write the Barcode Value to an NFC Tag

The Write to NFC Tag button stores the decoded barcode value into a nearby tag using NDEFReader.write():

function writeNFCTag(message){
  alert("Put the device close to the tag to write");
  const ndef = new NDEFReader();
  ndef.write(
    message
  ).then(() => {
    alert("Message written.");
  }).catch(error => {
    alert(`Write failed :-( try again: ${error}.`);
  });
}

Step 6: Decrypt the Barcode Content with the NFC Key

When both an NFC tag and a barcode have been scanned, the app uses the NFC value as the AES key to decrypt the barcode content with CryptoJS:

function decrypt(){
  if (NFCResults.length>0 && barcodeResults.length>0) {
    const message = barcodeResults[0].text;
    const record = NFCResults[0];
    const buf = record.data.buffer;
    const key = new TextDecoder().decode(buf);
    const bytes = CryptoJS.AES.decrypt(message,key);
    const originalText = bytes.toString(CryptoJS.enc.Utf8);
    document.getElementById("decrypted").innerText = originalText;
  }else{
    alert("Please scan the NFC tag and barcode first.");
  }
}

All right, we’ve now finished building the web app to scan both NFC tags and barcodes. Run it in Chrome on Android to scan a tag, or use the Simulate NFC Tag button in a desktop browser, and read the encrypted QR code generated by the generator.html sample in the repository.

Common Developer Questions

How do I scan NFC tags in a web app?

Use the NDEFReader API, available in Chrome for Android since version 89. Call ndef.scan() to start reading nearby tags and listen to the onreading event, which provides the tag records. The scan can be cancelled with an AbortController.

What happens if the browser does not support NFC?

If NDEFReader is not available (for example, on a desktop browser), the app disables the real NFC scan button and shows a Simulate NFC Tag button instead. It asks you for a key with prompt(), builds a record that mimics an NDEF record, and then runs the same display and decryption flow — so you can try the full workflow without NFC hardware.

How do I initialize Dynamsoft Barcode Reader in a browser?

Call LicenseManager.initLicense() to set the license, load the core module with CoreModule.loadWasm(['DBR']), then create a CaptureVisionRouter with CaptureVisionRouter.createInstance(). This is the current Capture Vision API, which replaces the older Dynamsoft.DBR.BarcodeReader interface.

How do I scan a barcode from the camera?

Open the camera with getUserMedia, draw each video frame to a canvas, and pass it to cvr.capture(canvas, 'ReadBarcodes_Default'). Filter result.items for CRIT_BARCODE items and read item.text. Because capture() cannot run concurrently, guard it with a mutex.

Does camera scanning require HTTPS?

Yes. Access to the camera through getUserMedia requires a secure context, so the app must be served over HTTPS or localhost. NFC scanning with NDEFReader also requires a secure context.

Can the app read encrypted QR codes?

Yes. The app scans the encrypted QR code with Dynamsoft Barcode Reader and then decrypts it with CryptoJS using the AES key stored in an NFC tag. The generator.html sample in the repository creates such an encrypted QR code.

Source Code

Get the complete sample project source code on GitHub.

References