Back to BlogIndustry Guides

Retail Inventory Management Software for Small Business: When Custom Pays

Rupak Amin

Founder & Lead Engineer, RAITHub

13 min read

Most small retailers should buy inventory software, not build it: Zoho Inventory starts at $29 a month billed annually, and Lightspeed Retail at $89 a month. Custom pays when your stock rules are the business, such as batch expiry with first-expiring, first-out allocation, wholesale tiers, or several channels drawing on one ledger. Sundor Skin's custom wholesale platform runs those rules in a 146-table PostgreSQL core.

This guide is for owners and operations leads at shops with one to ten locations, in any country, who are deciding between a subscription and a custom system. It covers what current plans cost, the signs you have outgrown them, how FEFO allocation works in code, and when a subscription is still the right answer. Vendor prices are quoted from each vendor's own pricing page, checked on 30 September 2026. They change often, so check again before you decide.

What does retail inventory software cost for a small business?

Between nothing and a few hundred dollars a month for the software, before payment processing and hardware. The published entry points look like this:

ProductPublished priceWhat the plan page says about stock
Zoho InventoryFree plan (50 orders, 2 locations); Standard $29 or £25 a month; Professional $79 or £65 a month, billed annuallyProfessional lists batch and serial number tracking, 3,000 orders a month and 4 locations (Zoho Inventory pricing, UK pricing)
Lightspeed RetailBasic $89, Core $149, Plus $289 a monthInventory management and purchase order sync on all three; multi-location on custom pricing (Lightspeed Retail pricing)
Shopify POS ProUS$89, £69 or A$129 a month per location, on top of a Shopify planStock counts, purchase orders and low-stock alerts (Shopify POS pricing, UK, Australia)
Square for RetailFree plan; Plus £49 a month per location in the UKPlus adds inventory management, stock intake and purchase orders; Premium adds transfer orders (Square for Retail pricing, UK)
Cin7 CoreStandard $349, Pro $599, Advanced $1,199 a monthLists batch and expiry tracking and unlimited inventory locations; POS is an add-on (Cin7 pricing)

Over three years, that is roughly $2,844 for Zoho Professional, $5,364 for Lightspeed Core, $6,408 for two Shopify POS Pro locations (before the Shopify plan itself) and $12,564 for Cin7 Core Standard. Those totals are simple arithmetic on the list prices above; add-ons, extra users and payment fees come on top. A custom build costs more than any of them up front, which is why it has to earn its place.

When is off-the-shelf inventory software enough?

When your stock is simple: you buy a product, put it on a shelf, sell it, and reorder it. That describes most small shops, and the products above were designed for them. Buy if all of these are true:

  • Every unit of a product is the same; you do not need to know which batch or expiry date a unit came from.
  • One price per product, or a small number of price lists your tool supports.
  • One or two sales channels that your tool already connects to.
  • Your team can live with the tool's order and returns workflow, even if it is not quite how you would design it.

If you sell mainly online and are weighing a hosted store against a custom one, Shopify vs custom ecommerce covers that decision, and Shopify's limitations lists the walls stores usually hit first.

What are the signs a small retailer has outgrown its inventory tool?

The clearest sign is a spreadsheet running beside the tool, because the tool cannot hold a rule your business depends on. Four situations come up again and again:

SituationWhat the subscription doesWhat custom does instead
Products expire: skincare, food, supplementsTracks batches on some plans; allocation rules may be manual or fixedAllocates every order from the batch that expires first, with a minimum shelf-life rule per customer or channel
You sell wholesale and retail from the same stockSeparate price lists, often separate apps or plan tiersOne ledger, with tier pricing and credit decided on the server; see B2B tier pricing without Shopify Plus
Stores, a web shop and a marketplace all sell the same unitsSyncs counts through connectors, which can lag or double-countA single stock ledger every channel writes to, with each sale recorded once
Staff need narrow permissions: pickers, counters, approversA few fixed rolesRoles and permissions shaped to your team and checked on every action

One of these alone is often solvable with a better plan or an add-on. Two or three together, plus a spreadsheet to reconcile them, is when custom starts to pay for itself.

How does FEFO stock allocation work?

FEFO means first-expiring, first-out: each order takes stock from the batch with the earliest expiry date, so older units leave before they expire on the shelf. FIFO, first-in, first-out, is the same idea ordered by the date a batch was received rather than its expiry. For products with a shelf life, FEFO is usually what you want.

The data model is small. Stock is held per batch, per location, rather than as one number per product:

CREATE TABLE stock_batches (
  id           bigserial PRIMARY KEY,
  variant_id   bigint  NOT NULL,
  location_id  bigint  NOT NULL,
  batch_code   text    NOT NULL,
  expires_on   date    NOT NULL,
  received_at  timestamptz NOT NULL DEFAULT now(),
  qty_on_hand  integer NOT NULL CHECK (qty_on_hand >= 0)
);

CREATE INDEX stock_batches_fefo
  ON stock_batches (variant_id, location_id, expires_on, id);

CREATE TABLE batch_allocations (
  order_line_id bigint  NOT NULL,
  batch_id      bigint  NOT NULL REFERENCES stock_batches (id),
  qty           integer NOT NULL CHECK (qty > 0),
  PRIMARY KEY (order_line_id, batch_id)
);

Allocation locks the usable batches in expiry order, takes what it needs from each, and records which batch every unit came from. This is a minimal TypeScript sketch with node-pg, written for this post; it is not Sundor Skin's code, which belongs to its owner.

import type { PoolClient } from 'pg'

export class InsufficientStock extends Error {
  constructor(public variantId: number, public short: number) {
    super('Not enough in-date stock for variant ' + variantId + ', short by ' + short)
  }
}

type Pick = { batchId: number; qty: number; expiresOn: Date }

// Call inside the order transaction (BEGIN ... COMMIT), so a shortfall rolls back everything.
export async function allocateFefo(
  client: PoolClient,
  orderLineId: number,
  variantId: number,
  locationId: number,
  qty: number,
  minShelfDays = 0, // e.g. 90 for a wholesale buyer who needs three months of shelf life
): Promise<Pick[]> {
  const { rows } = await client.query(
    'SELECT id, qty_on_hand, expires_on FROM stock_batches ' +
      'WHERE variant_id = $1 AND location_id = $2 AND qty_on_hand > 0 ' +
      'AND expires_on > current_date + $3::int ' +
      'ORDER BY expires_on, id FOR UPDATE',
    [variantId, locationId, minShelfDays],
  )

  const picks: Pick[] = []
  let remaining = qty
  for (const b of rows) {
    if (remaining === 0) break
    const take = Math.min(remaining, Number(b.qty_on_hand))
    await client.query('UPDATE stock_batches SET qty_on_hand = qty_on_hand - $2 WHERE id = $1', [b.id, take])
    await client.query(
      'INSERT INTO batch_allocations (order_line_id, batch_id, qty) VALUES ($1, $2, $3)',
      [orderLineId, b.id, take],
    )
    picks.push({ batchId: Number(b.id), qty: take, expiresOn: b.expires_on })
    remaining -= take
  }
  if (remaining > 0) throw new InsufficientStock(variantId, remaining)
  return picks
}

Three details carry the weight. FOR UPDATE locks the batch rows until the transaction ends, so a second order for the same product waits and then sees the reduced quantities; PostgreSQL documents that it "prevents them from being locked, modified or deleted by other transactions until the current transaction ends" (PostgreSQL: explicit locking). The CHECK constraint is a floor that makes negative batch stock impossible even if a future code path forgets the rule. And the minimum shelf-life parameter is where retail and wholesale differ: a shop may sell a unit expiring next month at a discount, while a wholesale buyer can refuse it. For FIFO, order by received_at instead of expires_on.

The wider rule, decrementing stock inside the order transaction so two customers cannot buy the last unit, is covered step by step in how to stop overselling with stock reservation. FEFO is the same pattern applied per batch.

How do you keep store sales and web sales from double-counting stock?

Record every stock change as a movement with a unique source reference, and apply it only if the movement is new. Point-of-sale (POS) systems and webhooks retry, so the same sale can arrive twice. Shopify's documentation, for example, says "your app might receive the same webhook more than once, for example after a network timeout or a retry" (Shopify: ignore duplicate webhooks).

CREATE TABLE stock_movements (
  id          bigserial PRIMARY KEY,
  source      text    NOT NULL,   -- 'pos', 'web', 'count', 'transfer', 'return'
  source_ref  text    NOT NULL,   -- the POS sale line ID, web order line ID, count ID
  variant_id  bigint  NOT NULL,
  location_id bigint  NOT NULL,
  delta       integer NOT NULL,   -- negative for a sale, positive for a receipt
  created_at  timestamptz NOT NULL DEFAULT now(),
  UNIQUE (source, source_ref)
);

-- Inside the same transaction as the stock update:
INSERT INTO stock_movements (source, source_ref, variant_id, location_id, delta)
VALUES ('pos', $1, $2, $3, $4)
ON CONFLICT (source, source_ref) DO NOTHING
RETURNING id;

If the insert returns a row, apply the change to stock. If it returns nothing, the sale was already recorded, so do nothing and report success. PostgreSQL states that "ON CONFLICT DO NOTHING simply avoids inserting a row as its alternative action", and that with RETURNING, "only rows that were successfully inserted or updated will be returned" (PostgreSQL: INSERT). The ledger also gives you something most small shops lack: a history of why stock is what it is.

What does a custom retail inventory system include?

Less than a full retail suite, if you scope it well. A useful first release usually has:

  1. Catalogue and variants. Products, sizes and colours, barcodes, each with its own stock.
  2. Batch stock per location. Receiving, expiry dates, transfers between locations, and stock counts that post adjustments to the ledger.
  3. Allocation rules. FEFO or FIFO, minimum shelf life, and which locations may fulfil which channel.
  4. Channel connections. Your web shop and POS writing sales to one ledger, each sale recorded once.
  5. Roles and an audit trail. Who received, adjusted or wrote off stock, and when. The pattern is in how to design an audit log.
  6. Reports that answer real questions. What expires in the next 60 days, what is not moving, what to reorder.

Purchase orders, supplier portals and demand forecasting can wait for a second release, or stay in a tool you already pay for. Many retailers keep their POS and accounting software and build only the stock core between them.

How does Sundor Skin handle batch stock and expiry?

Sundor Skin is a B2B skincare wholesaler that RAITHub designed and built end to end. It holds stock per batch with an expiry date, and allocates each order first-expiring, first-out. Pick, pack and dispatch steps are idempotent at the database level, and returns go through authorisation and inspection before restocked goods re-enter their original batch, so expiry tracking stays accurate. Credit checks, pricing and stock allocation run as database functions inside one transaction, so a bug in a screen cannot oversell stock.

The platform has 146 PostgreSQL tables with row-level security, 530+ automated tests, 88 permission codes, 12 staff roles, and Bronze to Platinum tier pricing with credit limits. The full build is on the Sundor Skin case study, and the wholesale side is explained in B2B ecommerce platform development.

Why RAITHub for this

  • Batch stock and FEFO in production. Sundor Skin allocates every order by batch expiry, with idempotent fulfilment steps and returns that restock into the original batch.
  • Stock rules in the database, tested. Allocation, pricing and credit run inside one transaction, covered by 530+ automated tests on Sundor Skin. Concurrency gets tests against a real database, not assumptions.
  • Scoped to the gap. Often the right build is a stock core between the POS and accounting tools you keep, not a replacement for everything.
  • Hours that reach you. RAITHub works from Dhaka (UTC+6). US and Canada clients get a daily 2-hour window, Dhaka 19:00 to 21:00, which is 08:00 to 10:00 in New York and Toronto. The UK gets 3 hours of overlap in winter and 4 in summer, Dubai 7, and Sydney 5 (4 during daylight saving). The working week is agreed per client; everything else runs on written daily handoffs.
  • Fixed scope, your code. A free 15-minute technical audit, then a written fixed quote. You own the code, and an NDA is standard. See the API and backend development service.

When you don't need us

  • Your stock is simple. One price, no batches, one or two channels: a subscription from the table above does the job for far less than a build.
  • A higher plan already has the feature. If batch tracking or multi-location is one tier up, pay for the tier and configure it.
  • You need a native mobile app for your staff. RAITHub builds web applications and PWAs, which run on tablets and phones in a browser, but does not offer native mobile app development.
  • You want developers placed in your team by the hour. RAITHub offers fixed-scope builds and dedicated teams, not staff augmentation.

For the wider store build, see custom ecommerce and marketplace development, the ecommerce industry page and RAITHub's work. If your stock already lives in a spreadsheet beside your tool, book the free 15-minute technical audit and bring the spreadsheet: it is the clearest spec there is.

Last reviewed: 30 September 2026. Vendor prices and documentation checked on 30 September 2026.

Frequently asked questions

What is a low-cost way to manage retail inventory for a small shop?

Start with a free or entry plan from an inventory or POS vendor. Zoho Inventory has a free plan for 50 orders and 2 locations, and Square for Retail has a free plan. Move up a tier only when you hit a specific limit.

When should a small retailer build custom inventory software?

When a rule your business depends on lives in a spreadsheet beside your tool: batch expiry with FEFO allocation, wholesale and retail sharing one stock pool, several channels double-counting, or staff permissions the tool cannot express. Two or more of these together usually justify a build.

What is the difference between FEFO and FIFO?

FIFO, first-in, first-out, ships the batch you received first. FEFO, first-expiring, first-out, ships the batch that expires first. They differ when batches arrive with different shelf lives, and FEFO is usual for skincare, food and supplements.

Can I keep my POS and still build custom inventory?

Often, yes. Many retailers keep their POS and accounting software and build only the stock core between them, with each POS sale written to one ledger once. Whether it works depends on your POS offering an API or webhooks.

How do I stop the same sale being counted twice?

Record each stock change with a unique source reference, such as the POS sale line ID, and use a unique constraint so a retried or duplicated message is ignored. Apply the stock change only when the movement is new.

Does RAITHub build retail inventory systems?

RAITHub builds the web side: stock ledgers, batch and FEFO allocation, channel sync, back-office portals and PWAs. Its proof is Sundor Skin, a B2B wholesale platform with FEFO fulfilment and 530+ tests. It does not build native mobile apps or claim POS hardware integrations.

retail inventory management softwareinventory software for small businesscustom inventory systemfefo allocationbatch expiry trackingmulti-location inventory

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.