Most businesses have no shortage of equipment records. The challenge begins when someone needs to identify a particular laptop, monitor, or other device and find its details quickly.

An asset might have a serial number, an internal inventory ID, a spreadsheet entry, and an employee assignment record. But connecting the physical device to its digital record can still take time.

QR codes provide a simple bridge between physical equipment and an asset management system.

In this tutorial, we'll design a basic QR-based IT asset tracking application using Node.js, Express, PostgreSQL, and the qrcode package.

We'll cover the data model, QR generation, asset lookup endpoints, and security considerations.

1. How QR-Based Asset Tracking Works

The concept is straightforward:

  1. Register an IT asset in a database.
  2. Generate a unique public identifier for that asset.
  3. Encode an asset lookup URL inside a QR code.
  4. Attach the QR code label to the physical equipment.
  5. Scan the code to open its digital asset record.

For example, a laptop might have:

Field Example
Asset Name Dell Latitude Laptop
Internal Asset ID LT-001
Serial Number DEVICE-SERIAL-001
Status Available
Location IT Storage Room

The QR code contains a lookup URL, rather than a serial number or confidential employee information.

That separation is important. A physical asset label can be seen or photographed by anyone nearby.

2. Technology Stack

We'll use:

  • Node.js: JavaScript runtime for the backend.
  • Express: HTTP routing and API handling.
  • PostgreSQL: Relational database for asset records.
  • pg: PostgreSQL client for Node.js.
  • qrcode: QR code generation.
  • dotenv: Local environment configuration.

Start a new project:

mkdir qr-asset-tracker cd qr-asset-tracker npm init -y npm install express pg qrcode dotenv 

Use a currently supported Node.js LTS release.

3. Create the Database Schema

For this example, we need an asset table containing an internal ID and a separate public lookup token.

CREATE TABLE assets ( id BIGSERIAL PRIMARY KEY, asset_code VARCHAR(50) UNIQUE NOT NULL, public_id UUID UNIQUE NOT NULL, name VARCHAR(150) NOT NULL, serial_number VARCHAR(150), status VARCHAR(30) NOT NULL DEFAULT 'available', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT valid_asset_status CHECK (status IN ( 'available', 'assigned', 'maintenance', 'retired' )) ); 

The asset_code is an internal business identifier, such as LT-001.

The public_id is a random UUID used in the QR lookup URL. This prevents the public URL from directly exposing sequential database IDs.

However, a UUID is an identifier, not an authentication mechanism.

4. Configure the Application

Create a .env file:

DATABASE_URL=postgresql://localhost:5432/asset_tracker PORT=3000 PUBLIC_BASE_URL=http://localhost:3000 ADMIN_API_KEY=replace-with-a-long-random-secret 

Create a server.js file:

require("dotenv").config(); const express = require("express"); const { Pool } = require("pg"); const QRCode = require("qrcode"); const { randomUUID, timingSafeEqual } = require("node:crypto"); const app = express(); app.disable("x-powered-by"); app.use(express.json({ limit: "10kb" })); const pool = new Pool({ connectionString: process.env.DATABASE_URL }); function requireAdmin(req, res, next) { const expected = process.env.ADMIN_API_KEY; const supplied = req.get("x-admin-key"); if (!expected || !supplied) { return res.sendStatus(401); } const a = Buffer.from(supplied); const b = Buffer.from(expected); if (a.length !== b.length || !timingSafeEqual(a, b)) { return res.sendStatus(401); } next(); } 

The administrator API key is used here only to keep the demonstration small. A production system should use proper user authentication and role-based authorisation.

5. Create an Asset Registration Endpoint

Next, register equipment and generate a random public lookup ID.

Add this endpoint to server.js:

app.post("/api/assets", requireAdmin, async (req, res) => { const { assetCode, name, serialNumber } = req.body; if ( typeof assetCode !== "string" || typeof name !== "string" || !assetCode.trim() || !name.trim() || assetCode.length > 50 || name.length > 150 || (serialNumber != null && (typeof serialNumber !== "string" || serialNumber.length > 150)) ) { return res.status(400).json({ error: "Invalid asset details" }); } try { const result = await pool.query( `INSERT INTO assets (asset_code, public_id, name, serial_number) VALUES ($1, $2, $3, $4) RETURNING id, asset_code, public_id, name, status`, [ assetCode.trim(), randomUUID(), name.trim(), serialNumber ?? null ] ); res.status(201).json(result.rows[0]); } catch (error) { if (error.code === "23505") { return res.status(409).json({ error: "Asset code already exists" }); } console.error("Asset registration failed"); res.sendStatus(500); } }); 

Parameterized SQL statements help prevent SQL injection.

For a production implementation, additional validation, permission checks, rate limiting, and audit logging should be included.

6. Generate QR Codes for Registered Assets

After registration, generate a QR code that links to the corresponding asset record.

app.get( "/api/assets/:publicId/qr", requireAdmin, async (req, res) => { try { const result = await pool.query( "SELECT public_id FROM assets WHERE public_id = $1", [req.params.publicId] ); if (!result.rowCount) { return res.sendStatus(404); } const assetUrl = `${process.env.PUBLIC_BASE_URL}/assets/` + result.rows[0].public_id; const qr = await QRCode.toBuffer(assetUrl, { type: "png", width: 300, margin: 2, errorCorrectionLevel: "M" }); res.type("png").send(qr); } catch (error) { console.error("QR generation failed"); res.sendStatus(500); } } ); 

A generated QR code can then be printed on a label and attached to the device.

For deployment, PUBLIC_BASE_URL should point to the application's actual HTTPS domain.

7. Retrieve Asset Details After Scanning

When the QR code is scanned, the browser opens a lookup URL.

For example:

https://assets.example.com/assets/{publicId}

That public-facing route should display only information authorised for the person scanning the code.

For this demonstration, we'll expose only basic identification information.

app.get("/assets/:publicId", async (req, res) => { try { const result = await pool.query( `SELECT asset_code, name FROM assets WHERE public_id = $1`, [req.params.publicId] ); if (!result.rowCount) { return res.sendStatus(404); } res.json(result.rows[0]); } catch (error) { console.error("Asset lookup failed"); res.sendStatus(500); } }); app.listen(process.env.PORT || 3000, () => { console.log("Asset tracker API started"); }); 

The route returns an identification record in JSON. A frontend can use the same endpoint to display a mobile-friendly asset page.

A complete application could let authorised employees view assignment history, maintenance records, or return status after logging in.

8. Test the Workflow

Start the server:

node server.js 

Create an asset:

curl -X POST http://localhost:3000/api/assets \ -H "Content-Type: application/json" \ -H "x-admin-key: YOUR_ADMIN_KEY" \ -d '{ "assetCode": "LT-001", "name": "Dell Latitude Laptop", "serialNumber": "DEVICE-SERIAL-001" }' 

The response contains a public_id.

Use that identifier to request the QR image through the authenticated QR endpoint:

GET /api/assets/{publicId}/qr

Scanning the QR code opens the asset lookup URL.

For testing on a physical phone, remember that localhost refers to the phone itself, not your development computer. Use a reachable development URL.

9. Security Considerations

QR-based asset tracking introduces several security concerns.

Never place confidential information directly in QR codes

Avoid encoding employee names, personal email addresses, internal credentials, or sensitive device information.

Authorise sensitive record access

Anyone who scans a label might obtain its URL. A random UUID does not replace authentication.

Restrict inventory modifications

Asset registration, assignment updates, deletions, and maintenance changes should be limited to authorised roles.

Protect against label replacement

A malicious person could replace a QR label with a different destination. Use trusted HTTPS domains, clearly branded labels, and appropriate physical verification procedures.

Maintain audit records

Recording who changed an asset's status and when is important for accountability. Audit events should be protected against unauthorised changes.

10. Moving From a Prototype to Production

The implementation above demonstrates the core identification flow. It is not a complete production asset management system.

A production-ready application would typically add:

  • Employee and department relationships
  • Equipment assignment and return workflows
  • Role-based access control
  • Maintenance history
  • Inventory reconciliation
  • Audit logs
  • Secure QR label printing
  • Rate limiting and operational monitoring

The goal is not merely to generate QR codes. It is to connect the physical equipment lifecycle with reliable digital records.

Conclusion

QR codes provide a practical way to connect physical IT equipment with a digital inventory system.

By combining unique public identifiers, a PostgreSQL database, and a Node.js API, developers can establish the foundation for equipment identification and retrieval.

From there, teams can extend the application with assignments, returns, maintenance tracking, and reporting.

For teams exploring practical equipment tracking workflows, Stackroom is another resource worth reviewing.

How would you handle QR code replacement, offline scanning, or asset ownership verification in a production system?

I'd be interested to hear how other developers approach these challenges.