A lightweight, barcode-driven point-of-sale system for family-run retail shops. Staff scan barcodes on a mobile app to build and complete sales. The web dashboard gives the owner full control over products, pricing, and revenue analytics from any browser.
Mobile App (Expo SDK 57 · React Native 0.86)
Scanner · Cart · Checkout · Sales History · Web Handoff
|
| HTTPS · Sanctum Token
v
Backend (Laravel 11 · PHP 8.3+)
REST API /api/v1 + Inertia.js Web Admin
|
v
PostgreSQL 16 (Docker · port 5433)
Web Admin (Inertia.js + React 19)
Dashboard · New Bill · Products · Sales · Settings
Sanctum Session Auth · Light / Dark / System theme
| Module | Description |
|---|---|
| Dashboard | Revenue by day / week / month / year; category pie chart with month ↔ year toggle; top-selling products |
| New Bill | Product search (case-insensitive), quantity controls, formatted discount input, numpad, live VietQR |
| Products | List, search, create, edit price / name / unit / category / status; auto-fetch product image |
| Sales | Paginated history, date filter, line-item drill-down, void a sale |
| Settings | Bank account configuration for VietQR (BIN, account number, holder name) |
| Theme | Light / Dark / System with persistent preference |
| Screen | Description |
|---|---|
| Login | Email + password, token stored in SecureStore |
| Scanner | Rear camera, debounced reads, manual barcode entry fallback |
| Product flow | Instant lookup on scan; inline create form if barcode is unknown |
| Cart | Edit quantities, remove items, running total |
| Checkout | Cash payment, server-side total validation, change calculation, receipt |
| Sales History | Recent transactions with line-item detail |
| Web Handoff | Send cart from mobile to web admin for desktop processing |
| Layer | Technology | Version |
|---|---|---|
| Backend | Laravel · PHP · PostgreSQL | 11 / 8.3+ / 16 |
| Web UI | Inertia.js · React · Tailwind CSS | v2 / 19 / v4 |
| Animation | Framer Motion | 12 |
| Charts | Recharts | 3 |
| Mobile | Expo · React Native · TypeScript | SDK 57 / 0.86 / 5 |
| Navigation | Expo Router | SDK 57 |
| State | Zustand | 5 |
| HTTP client | Axios | 1 |
| Auth | Laravel Sanctum | token (mobile) + session (web) |
| Payment QR | VietQR via img.vietqr.io | — |
| Build tool | Vite | 8 |
| Testing | Pest | 3 |
small-pos/
├── backend/ Laravel 11 — API + Web Admin
│ ├── app/
│ │ ├── Http/Controllers/
│ │ │ ├── Api/ AuthController, ProductController,
│ │ │ │ SaleController, PendingBillController
│ │ │ └── Web/ DashboardController, BillController,
│ │ │ ProductController, SaleController,
│ │ │ SettingsController
│ │ ├── Jobs/ FetchProductImage
│ │ └── Models/ User, Product, Sale, SaleItem,
│ │ ShopSetting, PendingBill
│ ├── resources/js/
│ │ ├── Components/ ThemeToggle, DataTable, SkeletonLoader
│ │ ├── Layouts/ AppLayout (sidebar + nav)
│ │ ├── Pages/ Dashboard, Bills/New, Products/Index,
│ │ │ Sales/Index, Sales/Show, Settings/Index
│ │ └── lib/ vietqr.ts, theme.ts
│ └── routes/
│ ├── api.php /api/v1/*
│ └── web.php Inertia page routes
│
├── mobile/ Expo SDK 57 — React Native
│ ├── app/
│ │ ├── (auth)/ login.tsx
│ │ └── (app)/ scanner.tsx, cart.tsx, sales/
│ └── src/
│ ├── lib/ apiClient.ts (Axios)
│ └── stores/ authStore, cartStore, apiUrlStore
│
├── docker-compose.yml PostgreSQL 16
└── README.md
| Tool | Version |
|---|---|
| PHP | 8.3+ with pdo_pgsql extension |
| Composer | 2.x |
| Node.js | 18+ |
| Docker | any recent version |
docker compose up -d| Setting | Value |
|---|---|
| Host | 127.0.0.1 |
| Port | 5433 |
| Database | small_pos |
| Username | small_pos |
| Password | secret |
cd backend
composer install
cp .env.example .env # skip if .env already exists
php artisan key:generate # skip if APP_KEY is already set
php artisan migrate
php artisan db:seed # creates the default owner account
npm install
npm run dev # Vite dev server with hot reload
php artisan serve # → http://localhost:8000Default credentials
owner@shop.local |
|
| Password | password |
Optional: run
php artisan queue:workin a separate terminal to enable automatic product image fetching. Everything else works without it.
cd mobile
npm install
npx expo start| Key | Action |
|---|---|
a |
Open Android emulator |
i |
Open iOS simulator |
| Scan QR | Open in Expo Go on a real device |
Default API URL: http://10.0.2.2:8000/api/v1 (Android emulator → host machine).
For a physical device or iOS simulator, create mobile/.env.local:
EXPO_PUBLIC_API_URL=http://<your-machine-ip>:8000/api/v1Base URL: /api/v1 — all endpoints require Authorization: Bearer <token> except login.
Response envelope
{ "success": true, "data": {}, "message": "OK" }
{ "success": false, "error": { "code": "...", "message": "...", "details": {} } }| Method | Endpoint | Description |
|---|---|---|
POST |
/auth/login |
Email + password login, returns Sanctum token |
POST |
/auth/logout |
Revoke current token |
GET |
/auth/me |
Authenticated user info |
| Method | Endpoint | Description |
|---|---|---|
GET |
/products?search= |
List / search (ILIKE, case-insensitive) |
POST |
/products |
Create product |
GET |
/products/by-barcode/{barcode} |
Lookup by barcode |
PATCH |
/products/{id} |
Update product fields |
| Method | Endpoint | Description |
|---|---|---|
POST |
/sales |
Submit checkout — total computed server-side |
GET |
/sales |
Paginated list |
GET |
/sales/{id} |
Detail with line items |
| Method | Endpoint | Description |
|---|---|---|
POST |
/pending-bills |
Send cart from mobile |
GET |
/pending-bills/waiting |
Bills waiting to load on web |
GET |
/pending-bills/{id} |
Bill detail |
PATCH |
/pending-bills/{id} |
Update status (loaded / closed) |
users id, name, email, password, role, timestamps
products id, barcode (unique), name, unit, price, cost_price,
image_url, category, status, timestamps
sales id, user_id, total_amount, amount_paid, change_due,
discount_amount, payment_method, status, created_at
sale_items id, sale_id, product_id,
barcode* ← snapshot at time of sale
product_name* ← unaffected by future edits
unit_price* ← historical price preserved
quantity, line_total
shop_settings id, key, value, timestamps
bank_accounts id, label, bank_bin, bank_name, account_number,
account_holder, is_default, timestamps
pending_bills id, code, items (JSON), status, timestamps
personal_access_tokens Sanctum — mobile token storage
cd backend
./vendor/bin/pestUses SQLite in-memory — Docker is not required.
| Test case |
|---|
| Barcode uniqueness enforced on product creation |
| Product lookup — found / not found by barcode |
| Checkout total computed server-side correctly |
| Invalid product rejected on sale creation |
Editing product price does not alter historical SaleItem.unit_price |
Download latest build: SmallPOS.apk
Install directly on any Android device (Settings → Install unknown apps).
Build from source:
cd mobile
npm install -g eas-cli
eas login
eas build:configure # first time only
eas build --platform android --profile previewcd backend
composer install --no-dev --optimize-autoloader
npm run build
php artisan config:cache
php artisan route:cache
php artisan view:cachePrivate — all rights reserved.