Case study · Enterprise warehouse inventory · Foresee Solutions

SmartScan

Every tag, accounted for.

RFID middleware that sits between Android handhelds and any ERP, starting with NetSuite. It tracks every physical unit by its tag, runs the warehouse's floor workflows, and posts only finished, consistent transactions to the ERP.

EPC
Role
Freelancer · end-to-end ownership
Client
InnovateNex, for Foresee Solutions
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.

SmartScan owns the warehouse state in between. Operators scan against it at floor speed. The ERP stays 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 has no ERP-specific code in it. An ERP is a connector class you register.

What I owned

Whiteboard to warehouse floor.

InnovateNex brought me in as a freelancer to deliver SmartScan for their client, Foresee Solutions. 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.

  • Put the middleware in charge of warehouse state and kept the ERP as the financial record — the call everything else hangs 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 client, 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 live on an Android RFID gun. The app drives the reader through a native module and scopes everything to their 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

A single conditional UPDATE claims the scanned tags. Whatever it doesn't return is rejected, with a 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

One active session per bin or order, via a partial unique index.

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

Durable outbox

Postgres first, Redis for dispatch, retries with backoff.

outboxBullMQERP
statusqueued

19 modules × 3 actions

Granular RBAC. Stock edits are split from catalog edits; force-releasing a lock needs delete rights.

view
manage
delete

Schema per tenant

New clients are cloned from a template schema.

root
tenant_acme
tenant_nova
template

Survives a dead battery

The scan buffer syncs to the server as a draft.

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

Five workflows, 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. Learn one, and you know them all.

Back office

When the ERP rejects a sync, you know why.

The admin dashboard is where supervisors map records to the ERP and watch the sync queue. Every failed post carries the exact payload sent and the ERP's response, so an integration dispute takes minutes, not a log search.

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).

Decisions that paid off

The ledger.

01

Track tags, not quantities

Every quantity is derived from tags that were physically scanned, so typing errors stop existing.

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. The second operator gets a 409, not a race.

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 — no log-diving.

06

Schema per tenant, fail-closed

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

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.

Let’s work together

Have a project in mind?

patharia52@gmail.com
Connect & Follow
Open to Work© 2026 Mustafa Patharia. All rights reserved.