Skip to main content
RapidDev - Software Development Agency

How to Build a Inventory Tracking Platform with Replit

Build an inventory tracking platform in Replit in 1-2 hours. Use Replit Agent to generate an Express + PostgreSQL app that tracks individual assets — laptops, equipment, tools — with unique asset tags, assignment history, QR codes, and maintenance schedules. No coding experience needed. Deploy on Autoscale.

What you'll build

  • Asset registry with unique auto-generated asset tags (AST-XXXX format) and QR code data
  • Assignment tracking system: check assets in and out with full history of who held each item
  • Maintenance scheduler with overdue alerts and upcoming maintenance dashboard
  • Category and condition management with color-coded condition indicators
  • Location registry linking assets to buildings, floors, and rooms
  • Audit log of all asset changes with who performed the action and when
  • RESTful API with viewport-friendly endpoints including QR code lookup by asset tag
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate13 min read1-2 hoursReplit FreeApril 2026RapidDev Engineering Team
TL;DR

Build an inventory tracking platform in Replit in 1-2 hours. Use Replit Agent to generate an Express + PostgreSQL app that tracks individual assets — laptops, equipment, tools — with unique asset tags, assignment history, QR codes, and maintenance schedules. No coding experience needed. Deploy on Autoscale.

What you're building

An inventory tracking platform solves a real problem for any team managing physical assets. When you have 50 laptops, 30 company phones, and a fleet of vehicles, tracking who has what — and whether it's overdue for maintenance — becomes critical. A spreadsheet falls apart the moment two people edit it simultaneously or when you need to scan a QR code on the factory floor.

Replit Agent generates the entire Express backend in a single prompt. You describe the asset model, the assignment workflow, and the maintenance schedule pattern, and Agent produces working routes, Drizzle schema files, and a React frontend. The built-in PostgreSQL database stores your asset registry persistently, and Drizzle Studio gives you a visual table editor for bulk data entry during initial setup.

The architecture is straightforward: an `assets` table with unique asset tags, an `asset_assignments` junction table tracking current and past assignments, a `maintenance_records` table for service history, and a `locations` table for physical placement. Express routes handle the check-in/check-out workflow, and a QR code lookup endpoint lets staff scan a printed label to instantly pull up any asset's current status.

Final result

A fully functional asset tracking platform with QR code labels, assignment history, maintenance scheduling, and a React dashboard showing overdue and upcoming maintenance — deployed on Replit Autoscale.

Tech stack

ReplitIDE & Hosting
ExpressBackend Framework
PostgreSQLDatabase
Drizzle ORMDatabase ORM
Replit AuthAuth
qrcodeQR Code Generation

Prerequisites

  • A Replit account (Free plan is sufficient)
  • Basic understanding of what a database table is (no coding experience needed)
  • List of asset categories you want to track (equipment, vehicles, electronics, etc.)
  • Optional: OpenCage or Mapbox API key if you want geocoded location support

Build steps

1

Scaffold the project with Replit Agent

Open Replit, create a new Repl, and use the Agent prompt below to generate the full Express + PostgreSQL project structure. Agent will create the Drizzle schema, routes, and React frontend in one shot.

prompt.txt
1// Type this into Replit Agent:
2// Build an asset tracking platform with Express and PostgreSQL using Drizzle ORM.
3// Create a shared/schema.ts with these tables:
4// - assets: id serial primary key, asset_tag text not null unique (format AST-XXXX),
5// name text not null, description text, category text not null (enum: equipment/vehicle/furniture/electronics/tools),
6// serial_number text, purchase_date date, purchase_cost integer, warranty_expiry date,
7// condition text default 'good' (enum: new/good/fair/poor/retired), image_url text,
8// qr_code_data text unique, created_at timestamp default now()
9// - locations: id serial, name text not null, building text, floor text, room text
10// - asset_assignments: id serial, asset_id integer references assets, assigned_to text not null,
11// assigned_by text not null, location_id integer references locations, assigned_at timestamp default now(),
12// returned_at timestamp, notes text
13// - maintenance_records: id serial, asset_id integer references assets, type text not null
14// (enum: inspection/repair/calibration/replacement), description text not null,
15// cost integer, performed_by text, scheduled_date date, completed_date date, next_due_date date,
16// created_at timestamp default now()
17// - audit_log: id serial, asset_id integer references assets, action text not null,
18// performed_by text not null, details jsonb, created_at timestamp default now()
19// Create Express routes for all CRUD operations plus:
20// - POST /api/assets/:id/assign (check out to a person)
21// - POST /api/assets/:id/return (check back in)
22// - GET /api/assets/due-maintenance (overdue items)
23// - GET /api/assets/:tag/qr (lookup by asset tag for QR scanning)
24// Use Replit Auth for authentication.
25// Build a React frontend with an asset registry table, asset detail page,
26// checkout/return form, and maintenance dashboard.

Pro tip: After Agent finishes, open Drizzle Studio (the database icon in the Replit sidebar) to verify all five tables were created correctly before adding any data.

Expected result: A running Express app with all tables created and a React frontend showing the asset registry. The console shows 'Server running on port 5000'.

2

Add the asset tag generator and QR code creator

Every asset needs a unique human-readable tag (AST-0042) that can be printed on a label and scanned later. Add the auto-increment tag generator and QR code data creator to the POST /api/assets route.

server/routes/assets.js
1const express = require('express');
2const { db } = require('../db');
3const { assets, auditLog } = require('../../shared/schema');
4const { eq, sql } = require('drizzle-orm');
5
6const router = express.Router();
7
8// Generate next asset tag like AST-0042
9async function generateAssetTag() {
10 const result = await db.select({ max: sql`MAX(CAST(SUBSTRING(asset_tag FROM 5) AS INTEGER))` }).from(assets);
11 const next = (result[0].max || 0) + 1;
12 return `AST-${String(next).padStart(4, '0')}`;
13}
14
15// POST /api/assets — create new asset with auto tag and QR data
16router.post('/', async (req, res) => {
17 try {
18 const assetTag = await generateAssetTag();
19 const qrCodeData = `${process.env.REPLIT_DEPLOYMENT_URL || 'http://localhost:5000'}/api/assets/${assetTag}/qr`;
20
21 const [asset] = await db.insert(assets).values({
22 ...req.body,
23 assetTag,
24 qrCodeData,
25 }).returning();
26
27 await db.insert(auditLog).values({
28 assetId: asset.id,
29 action: 'created',
30 performedBy: req.user?.name || 'system',
31 details: { assetTag },
32 });
33
34 res.status(201).json(asset);
35 } catch (err) {
36 res.status(500).json({ error: err.message });
37 }
38});
39
40// GET /api/assets/:tag/qr — lookup by QR scan
41router.get('/:tag/qr', async (req, res) => {
42 const [asset] = await db.select().from(assets).where(eq(assets.assetTag, req.params.tag));
43 if (!asset) return res.status(404).json({ error: 'Asset not found' });
44 res.json(asset);
45});
46
47module.exports = router;

Pro tip: Install the `qrcode` npm package by typing `/qrcode` in the Replit packages panel. Then add a GET /api/assets/:id/qr-image route that generates an SVG QR code — this is what you print on physical labels.

Expected result: Creating a new asset returns a JSON object with an auto-generated asset_tag like 'AST-0001' and a qr_code_data URL that resolves to the asset's detail page.

3

Build the assignment checkout and return workflow

The core of asset tracking is knowing who has what right now. The assign route closes any open assignment first, then opens a new one. The return route sets returned_at on the current assignment.

server/routes/assignments.js
1const { assetAssignments, assets, auditLog } = require('../../shared/schema');
2const { eq, isNull, and } = require('drizzle-orm');
3
4// POST /api/assets/:id/assign
5router.post('/:id/assign', async (req, res) => {
6 const assetId = parseInt(req.params.id);
7 const { assignedTo, locationId, notes } = req.body;
8
9 try {
10 // Close any open assignment
11 await db.update(assetAssignments)
12 .set({ returnedAt: new Date() })
13 .where(and(
14 eq(assetAssignments.assetId, assetId),
15 isNull(assetAssignments.returnedAt)
16 ));
17
18 // Create new assignment
19 const [assignment] = await db.insert(assetAssignments).values({
20 assetId,
21 assignedTo,
22 assignedBy: req.user?.name || 'unknown',
23 locationId: locationId || null,
24 notes: notes || null,
25 }).returning();
26
27 await db.insert(auditLog).values({
28 assetId,
29 action: 'assigned',
30 performedBy: req.user?.name || 'unknown',
31 details: { assignedTo, locationId },
32 });
33
34 res.status(201).json(assignment);
35 } catch (err) {
36 res.status(500).json({ error: err.message });
37 }
38});
39
40// POST /api/assets/:id/return
41router.post('/:id/return', async (req, res) => {
42 const assetId = parseInt(req.params.id);
43
44 const [updated] = await db.update(assetAssignments)
45 .set({ returnedAt: new Date() })
46 .where(and(
47 eq(assetAssignments.assetId, assetId),
48 isNull(assetAssignments.returnedAt)
49 ))
50 .returning();
51
52 if (!updated) return res.status(404).json({ error: 'No active assignment found' });
53
54 await db.insert(auditLog).values({
55 assetId,
56 action: 'returned',
57 performedBy: req.user?.name || 'unknown',
58 details: {},
59 });
60
61 res.json(updated);
62});
63
64module.exports = router;

Expected result: POST /api/assets/1/assign returns the new assignment record. POST /api/assets/1/return closes the open assignment and sets returned_at to now.

4

Add the maintenance scheduler and overdue alerts

Maintenance records track service history and schedule future work. The GET /api/assets/due-maintenance endpoint returns assets where any maintenance record's next_due_date is today or earlier — this powers the overdue dashboard.

server/routes/maintenance.js
1const { maintenanceRecords, assets } = require('../../shared/schema');
2const { lte, eq } = require('drizzle-orm');
3
4// GET /api/assets/due-maintenance
5router.get('/due-maintenance', async (req, res) => {
6 const today = new Date().toISOString().split('T')[0];
7
8 const overdue = await db
9 .select({
10 assetId: assets.id,
11 assetTag: assets.assetTag,
12 assetName: assets.name,
13 category: assets.category,
14 maintenanceType: maintenanceRecords.type,
15 nextDueDate: maintenanceRecords.nextDueDate,
16 lastCompleted: maintenanceRecords.completedDate,
17 })
18 .from(maintenanceRecords)
19 .innerJoin(assets, eq(maintenanceRecords.assetId, assets.id))
20 .where(lte(maintenanceRecords.nextDueDate, today));
21
22 res.json(overdue);
23});
24
25// POST /api/assets/:id/maintenance — log or schedule maintenance
26router.post('/:id/maintenance', async (req, res) => {
27 const assetId = parseInt(req.params.id);
28 const { type, description, cost, performedBy, scheduledDate, completedDate, nextDueDate } = req.body;
29
30 const [record] = await db.insert(maintenanceRecords).values({
31 assetId,
32 type,
33 description,
34 cost: cost || null,
35 performedBy: performedBy || null,
36 scheduledDate: scheduledDate || null,
37 completedDate: completedDate || null,
38 nextDueDate: nextDueDate || null,
39 }).returning();
40
41 res.status(201).json(record);
42});

Pro tip: Add a compound index on maintenance_records(asset_id, next_due_date) in your Drizzle schema using `index('idx_maint_due').on(maintenanceRecords.assetId, maintenanceRecords.nextDueDate)` to keep the overdue query fast as records accumulate.

Expected result: GET /api/assets/due-maintenance returns an array of assets with overdue maintenance. An empty array means all assets are up to date.

5

Add the retry wrapper and deploy on Autoscale

Replit's built-in PostgreSQL goes idle after 5 minutes of inactivity. Add a retry wrapper around DB connection calls so the first request after idle wakes the database gracefully. Then deploy on Autoscale.

server/db.js
1// server/db.js — DB helper with retry wrapper
2const { drizzle } = require('drizzle-orm/node-postgres');
3const { Pool } = require('pg');
4
5const pool = new Pool({
6 connectionString: process.env.DATABASE_URL,
7 max: 10,
8 idleTimeoutMillis: 30000,
9 connectionTimeoutMillis: 5000,
10});
11
12exports.db = drizzle(pool);
13
14// Retry wrapper for the first query after DB idle
15exports.withRetry = async (fn, retries = 3) => {
16 for (let attempt = 1; attempt <= retries; attempt++) {
17 try {
18 return await fn();
19 } catch (err) {
20 const isConnectionError = err.code === 'ECONNREFUSED' || err.code === '57P03';
21 if (isConnectionError && attempt < retries) {
22 console.log(`DB connection attempt ${attempt} failed, retrying...`);
23 await new Promise(r => setTimeout(r, 1000 * attempt));
24 } else {
25 throw err;
26 }
27 }
28 }
29};
30
31// server/index.js — bind to 0.0.0.0 for Replit
32const app = require('express')();
33app.listen(5000, '0.0.0.0', () => console.log('Asset tracker running on port 5000'));

Pro tip: To deploy: click the Deploy button in the Replit toolbar, choose Autoscale, set the run command to `node server/index.js`. Autoscale scales to zero when idle, which is perfect for asset tracking — it's used during check-in/check-out cycles, not 24/7.

Expected result: The app deploys to a public URL. The retry wrapper handles the first DB query after any idle period without surfacing a 500 error to users.

Complete code

shared/schema.js
1const { pgTable, serial, text, integer, numeric, boolean, timestamp, date, jsonb } = require('drizzle-orm/pg-core');
2
3exports.assets = pgTable('assets', {
4 id: serial('id').primaryKey(),
5 assetTag: text('asset_tag').notNull().unique(),
6 name: text('name').notNull(),
7 description: text('description'),
8 category: text('category').notNull(),
9 serialNumber: text('serial_number'),
10 purchaseDate: date('purchase_date'),
11 purchaseCost: integer('purchase_cost'),
12 warrantyExpiry: date('warranty_expiry'),
13 condition: text('condition').default('good'),
14 imageUrl: text('image_url'),
15 qrCodeData: text('qr_code_data').unique(),
16 createdAt: timestamp('created_at').defaultNow(),
17});
18
19exports.locations = pgTable('locations', {
20 id: serial('id').primaryKey(),
21 name: text('name').notNull(),
22 building: text('building'),
23 floor: text('floor'),
24 room: text('room'),
25});
26
27exports.assetAssignments = pgTable('asset_assignments', {
28 id: serial('id').primaryKey(),
29 assetId: integer('asset_id').references(() => exports.assets.id).notNull(),
30 assignedTo: text('assigned_to').notNull(),
31 assignedBy: text('assigned_by').notNull(),
32 locationId: integer('location_id').references(() => exports.locations.id),
33 assignedAt: timestamp('assigned_at').defaultNow(),
34 returnedAt: timestamp('returned_at'),
35 notes: text('notes'),
36});
37
38exports.maintenanceRecords = pgTable('maintenance_records', {
39 id: serial('id').primaryKey(),
40 assetId: integer('asset_id').references(() => exports.assets.id).notNull(),
41 type: text('type').notNull(),
42 description: text('description').notNull(),
43 cost: integer('cost'),
44 performedBy: text('performed_by'),
45 scheduledDate: date('scheduled_date'),
46 completedDate: date('completed_date'),
47 nextDueDate: date('next_due_date'),
48 createdAt: timestamp('created_at').defaultNow(),
49});
50
51exports.auditLog = pgTable('audit_log', {
52 id: serial('id').primaryKey(),
53 assetId: integer('asset_id').references(() => exports.assets.id),
54 action: text('action').notNull(),
55 performedBy: text('performed_by').notNull(),
56 details: jsonb('details'),
57 createdAt: timestamp('created_at').defaultNow(),
58});

Customization ideas

Mobile QR scanner

Add a 'Scan Asset' button on the frontend that opens the device camera using navigator.mediaDevices.getUserMedia and a QR decoder library like jsQR to instantly look up any asset by scanning its printed label.

Bulk import from CSV

Add a CSV upload endpoint that parses a spreadsheet of assets and bulk-inserts them, auto-generating asset tags and QR codes for each row — ideal for onboarding an existing asset registry.

Depreciation tracking

Add a calculated depreciation field to the asset detail page using straight-line depreciation: (purchase_cost - salvage_value) / useful_life_years. Display current book value alongside purchase cost.

Email maintenance reminders

Add a daily cron job (Scheduled Deployment on Replit) that queries due-maintenance assets and sends reminder emails via SendGrid with the asset tag, overdue description, and a link to the maintenance form.

Common pitfalls

Pitfall: Assigning an asset without closing the previous assignment

How to avoid: The assign route always runs an UPDATE to close any open assignment (returnedAt IS NULL) before creating the new one. This is handled in a single DB transaction to prevent race conditions.

Pitfall: Hardcoding the asset tag URL in QR code data

How to avoid: Use process.env.REPLIT_DEPLOYMENT_URL to build the QR code URL. Add this to Replit Secrets so it updates when you redeploy to a custom domain.

Pitfall: Not adding an index on asset_tag for QR scan lookups

How to avoid: The unique constraint on asset_tag automatically creates a B-tree index in PostgreSQL. Verify it exists using Drizzle Studio's schema view.

Pitfall: Forgetting the DB retry wrapper for the first query after idle

How to avoid: Wrap all DB calls in the withRetry helper that catches ECONNREFUSED errors and retries with exponential backoff up to 3 times.

Best practices

  • Store REPLIT_DEPLOYMENT_URL in Replit Secrets (lock icon) so QR code URLs automatically point to your production domain after deployment.
  • Use Drizzle Studio (database icon in Replit sidebar) to bulk-import your initial asset list by pasting rows directly into the table editor.
  • Add a unique constraint on asset_tag in the Drizzle schema — Drizzle enforces this at the database level, not just in application code.
  • Log every action (create, assign, return, maintenance) to the audit_log table so you have a complete chain of custody for each asset.
  • Use the Haversine formula in a custom SQL query if you need to find all assets within a geographic radius of a location.
  • Deploy on Autoscale for asset tracking — usage is bursty (during audits and check-in/out cycles) rather than continuous, so Autoscale's scale-to-zero saves cost.
  • Print QR labels using a Brother or Zebra label printer connected to any computer — the QR code SVG from the qrcode package can be sent directly to a label printer.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building an asset tracking platform with Express and PostgreSQL. I have an assets table with asset_tag (unique), condition, and category columns, and an asset_assignments table tracking who has each asset with assigned_at and returned_at timestamps. Help me write an Express route POST /api/assets/:id/assign that: (1) closes any open assignment by setting returned_at = now() where returned_at IS NULL, (2) creates a new assignment record, and (3) inserts an audit_log entry — all in a single transaction using node-postgres.

Build Prompt

Extend the asset tracking platform with a printable QR label generator. Add a GET /api/assets/:id/label route that generates an SVG containing the QR code (using the qrcode npm package) alongside the asset tag, name, and category in a 2-inch by 1-inch label format. On the React frontend, add a Print Labels button that opens a print-preview dialog showing selected assets as a grid of labels — clicking Print sends the SVGs to the browser's print API for direct label printing.

Frequently asked questions

Can I import my existing asset list from a spreadsheet?

Yes. Export your spreadsheet as CSV, then add a POST /api/assets/import route that uses the csv-parse npm package to read the file and bulk-insert rows. Drizzle's db.insert().values(arrayOfRows) handles batch inserts in one query. Asset tags and QR codes are auto-generated for each new row.

How do I generate printable QR code labels?

Install the qrcode npm package. Add a GET /api/assets/:id/qr-svg route that returns an SVG QR code. On the React frontend, show a Print Labels button that renders selected assets' QR SVGs in a grid and triggers window.print(). Use CSS @media print to hide all UI except the label grid.

What Replit plan do I need?

The Free plan is sufficient to build and test the app. For deployment with a public URL, Replit Autoscale works on all paid plans starting at Core. The built-in PostgreSQL is available on all plans including Free during development.

How does the assignment history work for compliance audits?

Every assignment record has a returned_at timestamp. To see the full history of an asset, query asset_assignments WHERE asset_id = ? ORDER BY assigned_at DESC. The audit_log table also records every action with performer name and timestamp. Both tables are queryable in Drizzle Studio.

How do I handle assets that are permanently retired?

Set the asset's condition to 'retired' via PATCH /api/assets/:id. Add a filter to the default asset list query to exclude retired assets: WHERE condition != 'retired'. Create a separate Retired Assets view in the frontend for the rare case when you need to look up historical records.

Can multiple people use the app simultaneously?

Yes. Replit's PostgreSQL handles concurrent connections. The assign route uses a WHERE returnedAt IS NULL check to prevent double-assignments. For high-concurrency scenarios, add a SELECT ... FOR UPDATE lock on the asset row before the assignment update.

Can RapidDev help build a custom asset tracking platform?

Yes. RapidDev has built 600+ apps including inventory and operations tools. They can add custom workflows like multi-site tracking, approval flows for high-value assets, or integrations with your existing ERP system. Book a free consultation at rapidevelopers.com.

What happens if the database goes idle and a user scans a QR code?

The withRetry wrapper in server/db.js catches the connection error on the first query after idle and retries up to 3 times with a 1-second delay between attempts. The QR scan typically succeeds on the second attempt, with a ~2 second delay that users rarely notice.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.