Offline-first desktop inventory tracker for panel & body manufacturing component tracking.
Built with Electron, React, and a local SQLite database β no server, no internet connection, no monthly bill. Just fast, indexed, reliable inventory control that lives on the shop floor's own machine.
Stockyard was built to replace manual spreadsheet reconciliation for tracking physical components on a manufacturing floor β panels, body parts, fasteners, sub-assemblies β with a single-file, installable desktop application that works entirely offline. It reduced manual reconciliation effort by up to 66% by moving stock counts, low-stock alerts, and bill-of-materials coverage checks out of shared spreadsheets and into a real, indexed, transactional database with a purpose-built UI.
It runs as a native Windows desktop app: double-click, no login, no cloud dependency, no internet required. All data lives in a single SQLite file on the machine it's installed on.
Spreadsheet-based inventory tracking on a manufacturing floor breaks down in predictable ways:
- No enforced structure β two people can name the same SKU differently, or overwrite each other's edits.
- No real audit trail β "who took the last 10 units, and when?" has no good answer.
- No automatic alerting β stockouts get discovered when someone walks to the shelf, not before.
- No fast lookup β searching a 5,000-row spreadsheet by SKU or location is slow and error-prone.
Stockyard solves each of these directly: every stock change is a recorded transaction (not an overwritten cell), every search hits an indexed SQL query instead of scanning rows in memory, and a background engine continuously watches stock levels so shortages surface automatically instead of being discovered on the floor.
- Full component catalog with SKU, name, category, quantity, unit, location, cost per unit, supplier, and free-form notes.
- Configurable low-stock threshold and optional max stock per item, with category-level aging windows (flag components that haven't moved in N days).
- Soft-delete support β items are archived, not destroyed, so historical transactions stay intact.
- Barcode scanner support out of the box: keyboard-wedge scanners are detected by keystroke timing (fast bursts terminated by
Enter), distinguishing a scan from normal typing without any special hardware driver.
At-a-glance KPI cards β total SKUs, out-of-stock count, low-stock warnings, aging components, and total inventory valuation β plus a critical-shortages table so the most urgent items are always visible first.
A scheduled job (interval configurable in Settings) continuously scans inventory and raises alerts with severity levels:
| Alert type | Severity | Trigger |
|---|---|---|
OUT_OF_STOCK |
Critical | Quantity reaches zero |
LOW_STOCK |
Warning | Quantity falls at/below the item's threshold |
AGING |
Info | No recorded movement within the category's aging window |
Alerts can be acknowledged (snoozed) or resolved, and trigger native OS notifications so a shortage doesn't sit unnoticed in a background window.
Every stock change β receipt, issue, adjustment, or initial count β is written as an immutable transaction row with the quantity delta, the quantity before the change, the operator who performed it, and an optional reference (PO number, build name). Nothing is ever silently overwritten.
Define BOM templates (a named list of components + required quantities) and instantly check coverage β how many complete units can be built right now given current stock β without manually cross-referencing the inventory sheet.
- One-click CSV export (native OS save dialog) for inventory, transactions, or alert history.
- One-click raw
.sqlitedatabase backup. - Factory reset / wipe for a clean slate on a new install.
Stockyard follows a multi-process local clientβserver pattern β the same shape as a web app's frontend/backend split, just running entirely on one machine. This keeps the UI thread free of database work and keeps filesystem/database access out of the (sandboxed) renderer entirely.
ββββββββββββββββββββββββββββββββββββββββ
β RENDERER PROCESS (React UI) β
β - Captures filters and input search β
β - Debounces input state (250ms) β
β - Renders paginated data tables β
β - Sandboxed: no direct filesystem access β
ββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β
β Asynchronous IPC Bridge (contextBridge, contextIsolation: true)
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β MAIN PROCESS (Node.js) β
β - Receives IPC requests (API gateway) β
β - Handles file I/O (CSV export, backup) β
β - Runs the background alert engine β
β - Fires native OS notifications β
ββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β
β Drizzle ORM β indexed SQL queries
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β SQLITE DATABASE (single file) β
β - better-sqlite3, synchronous, in-process β
β - Indexed on sku, name, category, location, β
β deletedAt, createdAt β
ββββββββββββββββββββββββββββββββββββββββ
- Process isolation. The renderer runs
contextIsolation: true,nodeIntegration: false,sandbox: true. It has zero direct filesystem or Node.js access β every operation crosses through a typedpreload.tsbridge intoipcMainhandlers in the main process. A compromised or buggy UI can't touch the database or disk directly. - Server-side filtering & pagination. Search, category filters, and status calculations run as SQL in SQLite, not as array filtering in React state β results are paged with
LIMIT/OFFSET(100 rows/page) to keep memory flat regardless of catalog size. - Indexed reads. Explicit indexes on the columns actually used for filtering and sorting (
sku,name,categoryId,location,deletedAt,createdAt) keep lookups fast even as the transaction ledger grows into the tens of thousands of rows. - Debounced input. Search inputs debounce at 250ms client-side before dispatching a query, avoiding a database round-trip on every keystroke.
- Background scheduled services. The alert engine runs on a timer inside the main process (independent of whether a UI window has focus), so alerts stay current even if the user is on a different tab.
- Append-only transactions. Stock changes are never in-place updates to a "current quantity" field alone β every change is logged as a new
transactionsrow with a before/after delta, making the transaction ledger a true audit trail.
| Layer | Technology | Purpose |
|---|---|---|
| Desktop shell | Electron 31 | Cross-platform native desktop runtime |
| Frontend UI | React 18 + TypeScript | Component-based renderer |
| Build system | Vite 5 + vite-plugin-electron |
Bundling & hot-reload for both processes |
| State management | Zustand 4 | Lightweight global store (inventory, alerts) |
| ORM | Drizzle ORM + drizzle-kit |
Type-safe schema, queries, and migrations |
| Database | SQLite via better-sqlite3 |
Synchronous, in-process, single-file storage |
| Charts | Recharts | Dashboard visualizations |
| Icons | Lucide React | Icon set |
| Packaging | electron-builder (NSIS) | Windows installer generation |
Five tables, fully typed end-to-end via Drizzle's inferred select/insert types:
| Table | Purpose |
|---|---|
categories |
Component categories, each with a configurable aging window (days) and a UI badge color |
items |
The component catalog β SKU, quantity, threshold, location, cost, supplier, soft-delete flag |
transactions |
Append-only stock movement ledger β type, quantity delta, before-quantity, operator, reference |
alerts |
Generated by the alert engine β type, severity, active/acknowledged/resolved state |
bom_templates / bom_items |
Named BOM configurations and their required component quantities (composite PK) |
All communication between the UI and the database goes through a fixed set of asynchronous IPC channels exposed via the preload bridge β effectively the app's internal API surface.
Categories
categories:getAllβ fetch all categoriescategories:create/categories:update/categories:delete
Items / Inventory
items:getAllβ raw dumpitems:getFilteredβ paginated + filtered (used by tables)items:searchAutocompleteβ quick SKU/name lookupitems:getLocationsβ distinct warehouse/shelf locationsitems:getSimpleListβ minimal metadata (ID, SKU, name)items:getByIdβ full detailitems:create/items:update/items:deleteitems:adjustStockβ register a stock adjustment transaction
Transactions
transactions:getAll/transactions:getFilteredtransactions:getOperatorsβ distinct operator listtransactions:getByItemβ history for one item
Alerts
alerts:getActive/alerts:getResolvedalerts:acknowledge/alerts:resolvealerts:updatedβ event fired when the background engine raises new alerts
Bill of Materials
bom:getAll/bom:getDetailsbom:create/bom:update/bom:deletebom:checkCoverageβ compare a BOM against current stock to compute buildable quantity
Export & Settings
csv:exportβ native save-dialog export to CSVsettings:getSchedulerInterval/settings:setSchedulerIntervalsettings:runAlertScanβ force an immediate scansettings:backupDBβ raw.sqlitefile backupsettings:resetDataβ factory reset
Stockyard/
βββ build/ # App icons & packaging static assets
βββ dist-installer/ # Packaged Windows installer output (generated)
βββ drizzle/ # SQL migrations (generated by drizzle-kit)
βββ electron/ # Main process (Node.js)
β βββ database/
β β βββ connection.ts # DB connection init + migration runner
β β βββ schema.ts # Table schemas, indexes, inferred types
β β βββ queries/ # Query handlers (items, transactions, alerts, bom, categories)
β βββ services/
β β βββ alertEngine.ts # Stock-level scan β alert generation
β β βββ scheduler.ts # Interval-based background scheduling
β β βββ notifier.ts # Native OS notification dispatch
β β βββ csvExport.ts # CSV export via native save dialog
β βββ main.ts # Entrypoint, window creation, IPC registration
β βββ preload.ts # contextBridge β the only surface the renderer can call
βββ src/ # Renderer process (React)
β βββ components/layout/ # App shell / navigation layout
β βββ hooks/ # useInventory, useAlerts, useBOM, useScanner
β βββ lib/ # Typed IPC client wrapper
β βββ pages/ # Dashboard, Inventory, Transactions, Alerts, BOM, Settings
β βββ store/ # Zustand stores (inventory, alerts)
β βββ main.tsx # React bootstrap
βββ docs/ # Technical specification
βββ package.json
βββ tsconfig.json
- Node.js β₯ 18
- npm
- Windows (current packaging target β see Platform support)
# install dependencies
npm install
# run the Vite dev server + hot-reloaded Electron shell
npm run dev# generate a new migration after editing electron/database/schema.ts
npm run db:generate
# open Drizzle Studio to inspect the local database
npm run db:studio# compile TypeScript and bundle optimized assets
npm run build
# package into a standalone Windows installer (.exe) β runs the build step automatically
npm run electron:buildThe installer is emitted to dist-installer/.
Stockyard stores everything in a single SQLite file inside the OS application-data directory β no external database, no network calls, and no telemetry:
| OS | Path |
|---|---|
| Windows | C:\Users\<Username>\AppData\Roaming\stockyard\inventory.db |
Use Settings β Backup Database to copy this file out for safekeeping, and Settings β Reset Data to wipe it and start clean.
Current electron-builder configuration targets Windows x64 (NSIS installer) only, matching where it's deployed today. The Electron/React/Drizzle core has no Windows-specific code paths, so macOS/Linux targets could be added to electron-builder.config.js without core changes if ever needed β just untested today.
- Renderer runs fully sandboxed (
sandbox: true,contextIsolation: true,nodeIntegration: false) β the UI cannot touch the filesystem, spawn processes, or access Node APIs except through the explicit, typed IPC surface documented above. - Operator names are trimmed and capped at 100 characters before being written to the transaction ledger.
- All queries go through Drizzle's parameterized query builder β no raw string-concatenated SQL.
Internal tool β Β© Aditya Singh. Not currently published under an open-source license.