Skip to content

POS frontend

Stack: Vue 3, Vuetify 3, Vue Router, Pinia, Vite; Electron for desktop; optional Laravel Echo (Pusher/Reverb) for live updates.

This is the app store staff actually use to run the till, manage inventory, and handle day-to-day operations. Unlike Admin, everything here runs in the context of one tenant.

A user has to resolve which tenant they’re operating as before anything else works:

  • Desktop/manual: user enters a client code on the TenantSetup screen → POST api/tenant/resolve/client-code → tenant ID stored → redirect to Auth.
  • Web (by domain): an Axios interceptor / bootstrap step calls GET api/tenant/resolve/domain and caches the result.

Tenant ID lives in localStorage (or the Electron store) under key x-tenant-id (a legacy x_tenant_id key is still read once, for migration). src/Services/TenantService.js owns this: getTenantId(), setTenantId(), clearTenantId(), resolveTenant(), getBaseURL(). The router redirects to /tenant-setup if no tenant is saved and the current route isn’t already TenantSetup, and the reverse once one is.

Once resolved, src/axios/index.js’s request interceptor attaches X-Tenant-ID to every API call automatically. You never set it by hand.

src/Composables/useWebSocket.js subscribes to tenant-scoped channels once tenant and user are available (wired in views/Home.vue):

  • tenant.{tenantId}: tenant-wide (admin notices, config, force refresh)
  • tenant.{tenantId}.user.{userId}: per-user (payment failures, permission changes)
  • tenant.{tenantId}.store.{storeId}: per-store (sales, payments, stock transfer)

src/echo/echo.js sends X-Tenant-ID for broadcast auth the same way Axios does for regular requests.

Path: src/modules/. Each module follows the same shape: index.js, pages/, services/, routes/, store/, components/, composables/. src/modules/index.js is the registry: it imports each module and exports modules, moduleRoutes (merged into the main router), and piniaStores. Not every module scaffolded here is active; check the registry for what’s actually wired in.

A store’s business_type (retail | restaurant) changes frontend behavior without being a separate build: restaurant mode enables accompanying-stock and hides certain sidebar items (Warehouses, Stock Transfers, GRNs, Quotations). Exposed via systemConfigStore.isRestaurantMode. See Architecture.

Entry: electron/main.js, wrapping this same SPA. Thermal receipt printing goes through IPC via electron-pos-printer. Auto-updates pull from https://download.dg-smartpos.co.ke. Build with pnpm run electron-build.

Terminal window
pnpm install
pnpm run dev # :3001
pnpm run build
pnpm run lint
pnpm run test # Vitest
pnpm run electron # Electron dev wrapper

Needs VITE_TITAN_API (API base URL) and, for live updates, VITE_REVERB_APP_KEY/VITE_REVERB_HOST/VITE_REVERB_PORT/VITE_REVERB_SCHEME.

Before building a new page or modal here, see Conventions → Frontend UI patterns. Page height (fillMainHeight) and modal header truncation both have a specific right answer that isn’t obvious from Vuetify’s defaults.