StyloTech product · Ref. SP-01

NexStock

Inventory, sales and point of sale for businesses that outgrew the spreadsheet.

Platforms

Web · iOS · Android

On the web
  • The NexStock dashboard on the web for Ravate & Fils Quincaillerie, an invented hardware retailer: Rs 1,813,130 of stock valued at cost, 24 products and 5 suppliers, the stock value over six months, and the five items that moved most in the last 30 days.
    Dashboard
  • The product catalogue on the web: 24 products with their SKUs, categories, stock on hand and selling and cost prices, led by cordless drills, angle grinders and hammers.
    Products
  • The invoice ledger on the web: Rs 1,319,355 collected across 485 paid invoices, Rs 77,234 pending and one invoice overdue, above the day’s till sales and account invoices.
    Invoices
On iOS and Android
  • NexStock’s home screen on the phone: Rs 1.81M of stock across all branches, filters by branch and period, the six-month stock value trend, and low-stock alerts led by pruning shears out of stock in Port Louis.
    Home
  • The till on the phone: a cordless drill and two hammer drills in the basket, Rs 13,370 to charge, with the catalogue filtered by category and a barcode scanner in the search field.
    Till
  • The stock history of an angle grinder: 120 in and 58 out, each sale listed with its branch, its POS reference and the stock it left, above the adjustments that restocked it.
    Stock history
  • The product list on the phone: 24 products, filters for low stock and out of stock, and each line with its SKU, its price and the units left.
    Products
Screens from the React web client in a desktop browser and from the Flutter app on an Android emulator, both against a throwaway database for an invented hardware retailer — three branches, 24 products and five months of till sales, entered through the API. Never production data.

Small businesses rarely fail on effort. They lose margin in the gaps — stock counted in one place and sold in another, a quotation that never became an invoice, a transfer nobody recorded. NexStock closes them by putting every sale, delivery, transfer and count through one stock ledger, branch by branch.

StyloTech’s role

NexStock is StyloTech’s own product. We define, design, build, run and support it — the Laravel API, the React web app, the Flutter app for iOS and Android, the operator console, the public site and the deployment.

Because we operate it ourselves, the architecture we recommend is architecture we live with: the business is checked on every request, roles are enforced by the server rather than the screens, module boundaries are enforced in CI, and every migration is proven against an empty database before a release.

Get the app

Both store listings are still to be published. Until then, the web app is live.

Platforms
  • Web
  • iOS
  • Android
Built with
Backend
  • Laravel 12
  • PHP 8.3
  • JWT
  • TOTP two-factor
  • DomPDF
Web client
  • React 18
  • TypeScript
  • Vite
  • Tailwind CSS
  • TanStack Query
  • Zod
Mobile client
  • Flutter
  • BLoC
  • go_router
  • Dio
  • mobile_scanner
Data
  • PostgreSQL 16
  • Redis
  • S3-compatible storage
Quality & delivery
  • PHPUnit
  • Deptrac
  • Larastan
  • Pint
  • GitHub Actions
  • Docker
What StyloTech delivered
  • Product strategy
  • UI/UX design
  • Design system
  • Technical architecture
  • Frontend development
  • Backend development
  • Mobile development
  • Database design
  • Integrations
  • Testing & QA
  • Deployment
  • Maintenance

What it does

Each part was built for a real job on the day, not for a feature list. The product’s own site walks through them in full.

01

Stock by branch, with a ledger

Every delivery, sale, adjustment and transfer writes a movement with the balance it left behind, and the branch’s count is updated under a row lock — so each number on screen has its history.

02

A till that keeps selling offline

The phone’s till sends each sale with its own reference. Without a connection it queues the sale under a provisional receipt number and syncs later; a replay returns the original invoice instead of selling twice.

03

A counter in the browser too

The web till works from the same catalogue and the same stock, with held orders and a cash register that is opened with a float and closed at the end of the day.

04

Transfers between branches

A transfer moves from draft to pending, in transit and completed. Pending reserves the stock at the source, dispatch takes it out and receipt puts it in — and a step out of order is refused.

05

Quotations that become invoices

A quotation converts into an invoice without re-entry, unless it has expired, been rejected or already been converted. A nightly job marks unpaid invoices overdue.

06

Credit notes and refunds

A refund at the till is recorded as a credit note, and raising one puts the goods back into stock. Credit notes carry their own payments.

07

Purchasing, received properly

Purchase orders run through approval to receipt. Each delivery gets its own goods received note, a receipt larger than the order is refused, and supplier bills are matched against both.

08

A supplier catalogue

Each supplier’s own SKU, cost price and lead time per product, with a preferred supplier marked for reordering.

09

Thirty-two reports

Stock, sales, purchasing and receivables reports generated on the server, saved, and e-mailed on a schedule — with CSV import and export of the catalogue and a full data export.

10

Documents in the business’s own style

A designer on the web sets the template, colour, font, header, footer and watermark for invoices, quotations and purchase orders; the phone prints receipts at 58 mm, 80 mm or A4.

11

Roles and two-factor sign-in

Owner, admin, manager and staff, enforced by the API on every route group. Two-factor sign-in by authenticator app with recovery codes, and an account locked after five failed attempts.

12

API keys and signed webhooks

Keys scoped to one business, and webhooks for invoices, quotations, payments and credit notes, signed with HMAC-SHA256, retried, and switched off after repeated failures.

One sale at the till, traced

A cashier rings up a cordless drill and two hammer drills on the phone and takes the payment. This is everything between tapping “Charge” and the stock ledger moving.

Read from pos_repository.dart, bootstrap/app.php, ResolveTenantContext, PosSaleController, RecordPosSaleAction and StockWriter. Every name below appears in the source exactly as written.

  1. 01

    The phone sends it once, or keeps it

    PosBloc submits the sale with its own client_reference and the sale time in UTC. Offline, or on a network error, the sale goes into the Outbox under a provisional OFF- receipt number and is sent when the connection returns.

    • PosSaleSubmitted
    • client_reference
    • Outbox
    • OutboxKind.sale
    What it says when it refuses

    A sale the server rejects on its merits is not queued: retrying a doomed request would hide the problem from the cashier, so it is shown to them instead.

  2. 02

    The checks at the door

    An API key, if one is presented, is resolved before anything else; the caller is rate-limited, and a platform under maintenance answers before any business logic runs.

    • AuthenticateApiKey
    • throttle:api
    • MaintenanceMode
    What it says when it refuses

    More than 300 requests a minute from one user gets RATE_LIMITED; during maintenance every request gets platform.maintenance with a 503.

  3. 03

    A signed-in user

    The till’s routes sit behind JWT authentication.

    • auth:api
    • AUTH_UNAUTHENTICATED
    What it says when it refuses

    No token, or an expired one, is answered 401 AUTH_UNAUTHENTICATED.

  4. 04

    …who belongs to this business

    The tenant middleware reads the business from X-Business-Id, loads the caller’s membership and binds the business for everything that follows — including the company scope on every query.

    • X-Business-Id
    • ResolveTenantContext
    • tenant_id
    • tenant_role
    What it says when it refuses

    A missing header, a business the caller does not belong to, a suspended membership or a suspended business each has its own key: TENANT_HEADER_MISSING, TENANT_NOT_FOUND, MEMBERSHIP_INACTIVE, TENANT_SUSPENDED.

  5. 05

    Any role may sell

    The till carries no role gate, on purpose: owners, admins, managers and staff all ring up sales. Elsewhere the same four roles are enforced on route groups, and a role that is not admitted is refused with a 403.

    • MembershipRole
    • owner
    • admin
    • manager
    • staff
  6. 06

    Validated, and placed in a branch

    Every line needs a whole quantity of at least one, and the total cannot be negative. The branch comes from X-Branch-Id, then the branch name, then the first branch allowed to sell; walk-in sales share one Walk-in customer, and a sale time in the future is pulled back to now.

    • RecordPosSaleRequest
    • X-Branch-Id
    • pos.no_branch
    What it says when it refuses

    A malformed sale is answered 422 validation_failed with the errors per field; a business with no branch that may sell gets pos.no_branch.

  7. 07

    An invoice, written once

    RecordPosSaleAction looks for the client_reference first. A replay from the Outbox returns the invoice it already created; anything new is written with its lines as a paid POS invoice, inside one database transaction.

    • RecordPosSaleAction
    • replayOf
    • invoices
    • invoice_items
  8. 08

    Stock moves through the Inventory contract

    Sales never touches the stock tables. It calls the Inventory module’s StockWriterContract, which locks the branch’s stock row, takes the quantity off and records a movement with the balance it left — bundles expanded into their components first.

    • StockWriterContract
    • withdraw
    • branch_stock
    • lockForUpdate
    • stock_movements
  9. 09

    Paid, announced, printed

    One payment row per tender, each capped at the total so change is never booked as revenue. The low-stock alert and the invoice event go out after the write, and the phone renders the receipt itself — 58 mm, 80 mm or A4.

    • payments
    • StockDepleted
    • InvoiceRaised
    • buildReceiptPdf

Engineering detail

Taken from the project source, not from marketing material.

API
Laravel 12 on PHP 8.3 as a modular monolith: 17 modules, each split into domain, application and infrastructure layers; 332 routes; 116 migrations building 94 tables in PostgreSQL 16; Deptrac reports 0 boundary violations and blocks CI
Tenancy
The business comes from X-Business-Id and the caller’s membership is re-checked on every request across 245 routes; role gates sit on route groups across 123 more; route-model binding runs after the tenant check, so a record from another business cannot be bound
Web client
React 18 with strict TypeScript and Vite, TanStack Router and Query, Zod at every API boundary, Tailwind CSS and Radix; 16 feature modules and 59 routes in English and French, plus an operator console on the same stack
Mobile client
Flutter with BLoC, go_router, Dio and get_it — 193 hand-written Dart files across 19 features and 85 routes, five bottom tabs, camera scanning, an offline outbox for five kinds of write, and receipts rendered on the device
Data & operations
PostgreSQL 16, Redis for cache and queues, S3-compatible object storage; separate worker and scheduler containers that mark overdue invoices, send expiry digests and back the database up every night
Quality gates
601 PHPUnit tests with 4,662 assertions, Pint and Deptrac blocking in CI and Larastan at level 6 reported; every migration applied to an empty PostgreSQL 16 and the production image’s caches built before it counts as deployable; 109 Flutter tests besides

Inventory, sales and point of sale for businesses that outgrew the spreadsheet.

NexStock — StyloTech product — StyloTech