Retail & POS
Retail Point of Sale
Indicative price, not an offer: EUR 1000-2000/year or EUR 2500-5000 one-off
Generated from docs/plan/paid/retail-point-of-sale/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Multi-store retail merchants operate a web-based POS client on tablets that syncs inventory and orders with the main AstroBaaS database. Barcode scanners, receipt printers, and card readers are supported via generic USB/Bluetooth drivers. The POS works offline and reconciles transactions when connectivity returns, preventing double-charges and stock inconsistencies.
The problem
Our retail store and e-commerce shop run as separate silos. When someone buys in-store, our till is separate software and never talks to the website—stock counts stay out of sync. A customer might order a red shirt online while the store is selling the last red shirt to a walk-in customer, and we ship the wrong item or refund after the fact. Staff have no view of web orders or inventory, and managers cannot see sales across both channels. We need one system.
What it does
- POS client application: a responsive web app deployable to tablets (iOS Safari, Android Chrome), accessible at
/posafter staff login - Product barcode scanning: USB barcode scanner integration via generic input passthrough (no specific vendor API), search-as-you-type with product image, name, SKU, stock level at this store
- Add-to-cart flow: scan or search product, select variant/options, enter quantity, set price (auto-populated, editable for staff override), add to cart with visual confirmation
- Cart display: running total with tax/shipping preview, item removal, quantity adjustment, line discounts per item
- Payment terminal integration: generic card-present detection (tablet has a paired Bluetooth payment terminal), POS sends total to terminal, waits for card swipe/tap, gets authorization response, records transaction
- Offline mode: when network drops, POS continues accepting sales; transactions are queued locally with timestamp and staff member id
- Sync engine: when connectivity returns, POS reconciles all queued transactions—verifies stock is still available, re-authorizes payments (with soft retry if declined), and uploads orders to central database
- Stock decrement: scanning a product decrements stock at the current store location in real-time; if stock is zero, scanner shows warning ‘Out of stock at this location’
- Multi-store view: staff can see stock levels across all store locations for the product they’re scanning (read-only from POS; stock allocation is an admin job)
- Receipt printing: USB thermal printer support, prints itemised receipt with store name, date, order number, totals, payment method, and staff member name
- Till reconciliation: daily report showing cash/card totals, refunds, discrepancies vs center database; exported for accounting
- Store location management: each POS device is bound to a store location; inventory sync respects location (store A stock separate from store B)
- Offline transaction ledger: local SQLite database on the tablet stores queued orders; after sync, old orders are archived to save space
- Admin dashboard: view sales by store, by day, by product; see pending sync queue across all POS devices; manual reconciliation button to force sync
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Specific barcode scanner vendor APIs—POS assumes generic USB input; if a merchant wants a scanner with proprietary Cloud features, they integrate that themselves (reason: hardware ecosystem is too large; generic USB HID is the seam)
- Thermal printer driver customization—POS prints via generic ESC/POS commands; merchant-specific receipt formats are handled via a template system, not by shipping vendor drivers (reason: receipt design is a merchant workflow, not a technical integration)
- Native mobile app—POS is a web app on Safari/Chrome; if a merchant wants offline-first native code, they build it separately using the inventory sync API (reason: maintaining three platforms—iOS, Android native, and web—is unsustainable; web + PWA covers both)
- Hardware lease or procurement—this feature integrates POS hardware; it does not source, finance, or support specific devices (reason: hardware is a merchant’s procurement decision)
- Integration with store security systems (cameras, access control)—this is inventory and orders only (reason: security systems are out of scope for commerce; if merchant wants that integration, they hook their own webhook)
- Real-time inventory allocation (store A can ‘hold’ stock from store B)—POS shows stock at all stores, but all sales decrement from the current store; inter-store transfers are a manual admin operation (reason: allocation rules are complex and merchant-specific; starting simple avoids deadlock scenarios)
- Labor tracking or staff time-clock—POS records which staff member rang up a sale; it does not track break times or shift hours (reason: labor law is jurisdiction-specific; timekeeping is a separate HR feature)
Data model
Schema v9 migration. New collection store_locations with: id, name, address, phone, manager_id, created_at, updated_at. New field on Product: inventory changes from single count to inventory_by_store (map of store_id => { qty, reserved } for accounting). New collection pos_transactions with: id, store_id, staff_id, items (same shape as Order.items), total_cents, payment_method, payment_status, receipt_number, synced (flag), created_at, synced_at. New collection sync_queue (ephemeral, per-device) with: transaction_id, status (pending|syncing|complete|failed), retry_count, last_error, created_at. Extend Order model: new field source (enum: ‘web’|‘pos’) and store_id (nullable, present only if source=‘pos’).
API
- POST /api/pos/login
- GET /api/pos/products
- POST /api/pos/transactions
- GET /api/pos/transactions/{id}
- POST /api/pos/sync
- GET /api/pos/sync/status
- GET /api/pos/inventory/{store_id}
- POST /api/pos/stock-decrement
- GET /api/pos/receipts/{id}
- POST /api/pos/reconciliation
- GET /api/pos/stores
- POST /api/pos/payment/authorize
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
New admin section ‘Retail > Point of Sale’. Tabs: (1) Store Locations—CRUD for stores, assign managers, see POS device registration. (2) Sales Dashboard—date range picker, totals by store and by day, chart of hourly sales. (3) Sync Queue—list of pending syncs across all POS devices, status, last error, manual retry button. (4) Daily Reconciliation—select date, view expected vs actual cash/card, reconcile button (admin confirms totals match till receipt). (5) Product Inventory—multi-store view of stock levels, reserve stock button (management operation), see which store(s) are low. Metrics: total POS revenue (all stores, all time), average transaction value, top-selling products by store.
The seam — why this is paid
Core owns: Order model, inventory system, payment processing, stock management via existing inventory API. Paid pack owns: POS client application (web), offline queue, sync engine, store location management, receipt templates. The payment terminal integration point is the payment/authorize API, which core’s payments layer implements; POS calls it and receives authorization, but the terminal driver itself (USB/Bluetooth discovery, card swipe parsing) is handled by the paid POS client, not core.
Requires hardware integration (barcode scanners, payment terminals), offline mode, and extensive support commitment.
Dependencies
- orders-api
- products-variants
- payments-processors
- staff-authentication
- store-locations
- multi-tenant-inventory
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- POS client loads on iPad Safari and Android Chrome, responsive layout adapts to 10-inch tablet, barcode scanner focus events auto-trigger product search without clicking a field
- Staff scans a product with qty=5 in stock; POS adds one unit to cart; displayed cart total includes tax, matches manual calculation to nearest cent
- Network drops while POS has 3 transactions queued; transactions are written to local SQLite; network reconnects, sync starts and completes all 3, audit log shows sync timestamp and staff member
- Product ‘Red Shirt’ has 2 units at Store A and 5 units at Store B; staff at Store A scans it, inventory display shows ‘This store: 1 left (5 at Store B)’; attempting to add 3 to cart shows out-of-stock warning
- Staff enters manual price override (e.g., apply 10% discount manually), override is recorded in transaction with manager_id and timestamp; receipt shows both original and override price
- Receipt prints within 2 seconds of payment authorization, includes order number, staff name, store name, items, totals, payment method; layout is readable on 80mm thermal paper
- Sync queue shows 5 pending transactions from Store A POS device; admin clicks ‘Retry All’, each transaction re-authorizes payment; if one fails (e.g., card declined), retry stops, admin sees error reason and can manually void/refund
- Daily reconciliation report for Store A on 2026-09-01 shows: cash sales EUR 523.45, card sales EUR 1,892.30, refunds EUR 45.00; admin confirms till receipt reads EUR 2,370.75, clicks ‘Reconcile’; report marks complete
- POS device offline for 4 hours accumulates 37 transactions; when back online, sync takes less than 30 seconds for successful transactions and reports all details (including rejected ones) to admin
- Two POS devices at same store cannot both decrement stock for the same unit (race condition test); second decrement fails with ‘Stock depleted’, second transaction is rejected by sync with reason ‘Inventory conflict’
Risks
Careless builds break: (1) Offline sync idempotency—if a transaction syncs, then network drops, then reconnects, the transaction may sync again, double-charging the customer; must track sync_id on both sides. (2) Stock oversell—store A and store B simultaneously sell the last unit of a product because each POS device had stale inventory; must enforce server-side stock checks during sync, not trust client counts. (3) Payment terminal heartbeat—tablet loses Bluetooth connection to terminal mid-transaction; POS thinks payment failed but card was charged; must poll terminal status and reconcile. (4) Receipt print jamming—printer runs out of paper mid-receipt, staff doesn’t notice; order exists but no paper trail; must validate printer before marking transaction complete. (5) SQLite corruption on tablet—local offline queue database is corrupted due to power loss, sync queue is lost, staff re-enters sales manually, doubles revenue in daily report; must use WAL mode and test power-loss scenarios. (6) Timezone misalignment—POS timestamps transactions in local timezone, server records UTC, daily reconciliation report groups by wrong date; must enforce UTC everywhere or explicitly communicate timezone context.
Commercial context
| Suggested price | EUR 1000-2000/year or EUR 2500-5000 one-off |
| Rival anchor | Webkul POS (offline capability, inventory sync) |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.