Back

Case study · Enterprise warehouse inventory

SmartScan

Every tag, accounted for.

I designed and built SmartScan, an RFID middleware platform that sits between Android handhelds and the ERP, beginning with NetSuite. It tracks every physical unit by its tag, runs the warehouse floor workflows, and posts only completed, consistent transactions to the ERP.

EPC
Role
Freelancer · end-to-end ownership
Client
InnovateNex
Timeline
Jun – Sep 2026 · live in production
Surfaces
REST API · Admin web · Android · AWS
+

API endpoints

+

Postgres tables

+

Permission modules

+

Handheld workflows

The problem

The floor moves faster than the ERP.

A handheld reader picks up dozens of tags a second. The ERP wants clean, complete transactions, one at a time, and it takes its time to answer. Wire the two together directly and you get timeouts on the floor, quantities typed in by hand, and no record of which physical unit went where.

I designed SmartScan to own the warehouse state between the two. Operators scan against it at floor speed, while the ERP remains the financial system of record and receives only the finished result.

01

No typed quantities

A quantity is the count of tags physically scanned. Operators never key in a number.

02

Nothing gets lost

Not on a dead battery, a dropped Wi-Fi link, or a Redis restart mid-sync.

03

Not married to one ERP

The core contains no ERP-specific code; each ERP is integrated through a registered connector class.

What I owned

Whiteboard to warehouse floor.

InnovateNex brought me in as a freelancer to deliver SmartScan for one of their clients. I owned the whole of it: the system design, the ERP integration, what shipped, the handheld experience, and the build and deploy.

Architecture

Designed the system before a line of it was written.

  • Made the middleware the owner of warehouse state, with the ERP remaining the financial system of record: the decision the rest of the architecture depends on.
  • Chose tag-level tracking over quantities, a Postgres outbox ahead of Redis, and database-enforced locks.
  • Made the ERP a pluggable connector, and multi-tenancy a schema per tenant, so one deployment serves many companies, on any ERP.
Technical Design DocumentArchitecture & data model

Under the hood

Correctness, enforced by the database.

Built for the handheld

Operators work on an Android RFID reader. I integrated the reader through a native Kotlin module and scoped every operation to the operator's warehouse.

E280·6894·A3F1E280·6894·A3F2E280·6894·B017E280·6894·C4D9
SmartScan handheld dashboard showing items in stock, accuracy rate and recent scans

One statement, every edge case

Scanned tags are claimed in a single conditional UPDATE. Any tag it does not return is rejected, with a specific reason for each.

UPDATE epc_tags SET state = 'in_stock'
 WHERE epc = ANY(:scanned) AND item_id = :line
   AND state = :expected RETURNING epc;
E280…A3F1claimed
E280…A3F2claimed
E280…B017wrong item
E280…A3F2duplicate

Session locks

A partial unique index enforces one active session per bin or order.

OP-1
BIN A-03-2
OP-2
409 Conflict

Durable outbox

Every ERP job is written to Postgres before dispatch through Redis, with retries and backoff.

outboxBullMQERP
statusqueued

Granular permissions

Permissions are defined across more than 15 modules and three actions. Stock edits are separated from catalogue edits, and force-releasing a lock requires delete rights.

view
manage
delete

Schema per tenant

Each tenant is provisioned with a dedicated schema, cloned from a template.

root
tenant_acme
tenant_nova
template

Survives a dead battery

The scan buffer is synchronised to the server as a draft, so no work is lost if a device's battery fails.

scan_sessions.draft_lines
0 tagssyncing draft

The life of one tag

EPC E280·6894·A3F1

Scroll to follow a single unit through the warehouse. Every hop is one row in its append-only history.

available
in_stock
in_transit
shipped
TAG

01 / 06 · CSV import · batch B-0142

Imported

The tag enters the registry bound to one SKU. It exists on paper, not on a shelf — and a re-import can't rebind it once it has history.

epc_tags.bin_history

{ "action": "import", "batch": "B-0142" }

On the floor

Every workflow, one scanning model.

One scanning model

Every workflow starts the same way — pick a document or a bin, scan, review, complete — and every one is scoped to the operator's warehouse. Once an operator learns one, the rest follow the same pattern.

Back office

When the ERP rejects a sync, you know why.

I built the admin dashboard, where supervisors map records to the ERP and monitor the sync queue. Each failed post retains the exact payload sent and the ERP's response, so integration issues can be resolved in minutes rather than through log searches.

app.smartscan / erp sync queue
ERP sync queue listing succeeded and failed ERP posts with the error returned

Architecture

The middleware is the only thing that talks to the ERP.

ClientsMiddlewareStateERPRFID HandheldAndroid · Urovo SDKAdmin DashboardNext.js 16SmartScan MiddlewareAuth · RBAC · tenant contextPostgreSQL 17Tenant schemas · outboxBullMQ / RedisDispatch onlyS3Item & user imagesERPNetSuite connector · Flow A in · Flow B out
JWT clientsState & storageERP sync

The ERP pushes items, bins and orders in over an API key (Flow A). Completed receipts, fulfillments, transfers and count variances go back out through the outbox (Flow B).

Key design decisions

The ledger.

01

Track tags, not quantities

Every quantity is derived from tags that were physically scanned, which eliminates manual entry errors.

02

One conditional UPDATE claims the tags

Wrong item, duplicate, already-shipped and two operators racing are all resolved by the database in one statement.

03

Locks live in the database

A partial unique index allows one active session per bin or order. A second operator receives a conflict response rather than a race condition.

04

Postgres outbox in front of Redis

Every ERP job is written to Postgres before it's queued. Losing Redis delays a post; it never drops one.

05

Keep the payload of every ERP attempt

When the ERP rejects a post, the queue shows what was sent next to what came back, so rejected posts can be diagnosed without searching the logs.

06

Schema per tenant, fail-closed

Each tenant has its own Postgres schema. A request without a resolved tenant is refused outright.

SmartScan was my first project working directly with RFID hardware, from the reader SDK to handling tag reads at floor speed. It also deepened my approach to integration design: synchronising with NetSuite idempotently, so that every transaction stays consistent across both systems and every change remains fully auditable.

Stack

API

NestJS 11TypeScriptDrizzle ORMPostgreSQL 17Redis 7BullMQPassport JWTSwagger

Admin web

Next.js 16React 19Tailwind v4shadcn/uiTanStack TableZustandZod

Handheld

Expo SDK 54React Native 0.81Kotlin native moduleUrovo RFID SDKMMKVEAS Build

Cloud

AWS EC2RDSS3ECRSSMTerraformGitHub Actions (OIDC)Caddy

Honest limits

  • One connector so far. The abstraction is proven against NetSuite only; SAP is the next class to write.
  • Android only. The reader SDK is Android, so the handheld app is too.
  • No inbound quantity sync. If someone edits a stock quantity by hand in the ERP, SmartScan doesn't pick it up — stock is expected to move through the floor, where every change is scanned.

Have something worth building?

Tell me what you're building and where it's stuck. Every message reaches me directly, and I reply personally.

Available for freelance© 2026 Mustafa Patharia. All rights reserved.