Executive Summary

Overall health and key metrics for the POS system codebase

62/100
Health Score
1,017
Tests Passing
12
Apps & Packages
72
Total Findings
10
Critical
16
High
33
Medium
13
Low
Key Known Issues
  • CRITICAL Sync version tracking uses per-row counters instead of a global monotonic sequence — stores can miss updates entirely
  • CRITICAL shouldRemoteWin() compares createdAt instead of updatedAt — recently updated records can be overwritten by older remote changes
  • CRITICAL Same JWT secret shared across HQ and all stores — a compromised store token works everywhere
  • CRITICAL Secrets (JWT, DB passwords, sync tokens) hardcoded in docker-compose.prod.yml in plain text
  • CRITICAL N+1 query in sales list endpoint — 350 queries per page load (50 sales)
  • CRITICAL Missing database indexes on FK columns (sale_items.sale_id, user_store_roles.user_id, sales.store_id) — full table scans at scale
Analysis Summary

This analysis covers 4 domains across the POS monorepo: Backend (Fastify, Drizzle, PostgreSQL, Auth, WebSocket, Testing — 19 findings), Frontend (Vue 3, Pinia, PrimeVue, Router, TypeScript, CSS — 33 findings), Architecture & Sync (sync protocol, data flow, offline, scale, deploy, security — 20 findings), and Tauri Desktop (3 apps, hardware integration — code quality observations).

The system is functional with 1,017 passing tests and a working multi-store deployment. The most urgent issues are in the sync protocol (version tracking bug, conflict resolution bug, no idempotency) and security (shared JWT secret, plaintext secrets, open CORS). The frontend has significant type safety gaps (duplicated interfaces, as unknown as casts) and i18n violations (hardcoded English strings).

Tech Stack

Technologies and their versions extracted from package.json and Cargo.toml files

Backend

Fastify
^5.2.0
HTTP Framework
🗃
Drizzle ORM
^0.45.1
ORM
🐘
PostgreSQL
16-alpine
Database
🔥
Redis
7-alpine
Queue / Cache
🔒
@fastify/jwt
^9.0.0
Auth
🔄
@fastify/websocket
^11.0.0
Realtime
🛠
BullMQ
^5.0.0
Job Queue (HQ)
🔐
bcryptjs
^3.0.3
Hashing
Zod
^3.24.0
Validation
📋
postgres.js
^3.4.0
PG Driver

Frontend

🎨
Vue 3
^3.5.0
UI Framework
🍪
Pinia
^2.3.0
State Management
💎
PrimeVue
^4.3.0
Component Library
🔌
Vue Router
^4.5.0
Routing
🌐
vue-i18n
^11.0.0
i18n (en + es)
📈
Chart.js
^4.5.1
Charts
Vite
^6.2.0
Build Tool

Desktop (Tauri)

All 3 Tauri apps (pos-terminal, hq-manager, store-manager) now import shared logic from @pos/pos-logic (12 Pinia stores, 3 composables, config, modules).

🖥
Tauri
2.x
Desktop Runtime
🦀
Rust (Tokio)
1.x
Backend (Tauri)
🔌
tokio-postgres
0.7
PG Driver (Rust)
🌐
reqwest
0.12
HTTP Client (Rust)
🔄
tokio-tungstenite
0.24
WS Client (Rust)

Tooling & Infrastructure

📦
pnpm
10.6.2
Package Manager
🌀
Turborepo
^2.4.0
Monorepo Build
🐳
Docker
Compose v2
Container
🔧
TypeScript
^5.7.0
Language
Vitest
^3.0.0
Test Runner
📖
VitePress
latest
Docs
Nginx
alpine
Web Server
🔀
Traefik
v2
Reverse Proxy
Coolify
--
PaaS / Hosting
📋
Drizzle Kit
^0.31.9
Migration Tool
📋
Node.js
>=20.0.0
Runtime

Architecture Diagram

Multi-store POS system with HQ hub and per-store servers

Clients
Web Client (PWA)
Vue 3 + PrimeVue + Pinia
HQ / Store / POS modes
POS Terminal
Tauri 2 + Rust + SQLite
Barcode, Receipt, Drawer
HQ Manager
Tauri 2 Desktop
Store Manager
Tauri 2 Desktop
Shared Logic
@pos/pos-logic
12 Pinia stores, 3 composables, config, modules
Imported by all 4 client apps
↓ HTTP / WebSocket ↓
Servers
HQ Server
Fastify 5 — Port 3000
Master data, Sync hub
Store 1 Server
Fastify 5 — Port 3002
ST01 (Downtown)
Store 2 Server
Fastify 5 — Port 3002
ST02 (Mall)
↓ ↓
Data Layer
HQ PostgreSQL
pos_hq — Port 5434
Store 1 PostgreSQL
pos_store — separate instance
Store 2 PostgreSQL
pos_store — separate instance
Redis
BullMQ job queue
↑ WebSocket Sync ↑
Stores maintain a persistent WebSocket connection to HQ. Sync uses HTTP pull/push with transactional outbox pattern. WS notifications trigger immediate sync cycles.

Monorepo Structure

pnpm workspaces + Turborepo — 6 apps, 7 shared packages

pos/ ├── apps/ │   ├── hq-server/ ← Fastify API, Drizzle, BullMQ, WebSocket │   ├── store-server/ ← Fastify API per-store, sync client │   ├── web-client/ ← Vue 3 PWA (shared HQ/Store/POS frontend) │   ├── pos-terminal/ ← Tauri 2 desktop POS │   ├── hq-manager/ ← Tauri 2 desktop HQ management │   └── store-manager/ ← Tauri 2 desktop store management ├── packages/ │   ├── shared-types/ ← TypeScript interfaces & types │   ├── i18n/ ← vue-i18n translations (en + es) │   ├── ui-kit/ ← Shared Vue components │   ├── tax-engine/ ← Tax calculation (basis points) │   ├── sync-protocol/ ← Sync message types │   ├── event-bus/ ← Cross-component events │   ├── backend-adapter/ ← Tauri invoke ↔ HTTP adapter │   └── pos-logic/ ← Shared Pinia stores, composables, config, modules ├── deploy/ ← Nginx configs, config.json per env ├── docs/ ← VitePress documentation site ├── tools/ ← Scripts (deploy, seed, i18n validate) ├── docker-compose.yml ├── docker-compose.prod.yml ├── turbo.json └── pnpm-workspace.yaml
Test Distribution

378 HQ server tests
185 store server tests
248 web client (Vue) tests
85 pos-logic tests
211 shared package tests
1,102 total

Deployment Targets

hq.pos.eddieadiaz.com
tienda1.pos.eddieadiaz.com
tienda2.pos.eddieadiaz.com
pos.eddieadiaz.com (portal)
docs.pos.eddieadiaz.com

Key Conventions

Money: cents (integers)
Tax: basis points (825 = 8.25%)
Quantities: ×1000
Auth: PIN or password (bcrypt)
i18n: en + es required

Backend Analysis

Fastify, Drizzle ORM, PostgreSQL, Authentication, WebSocket, Testing

Fastify Server

Medium Zod Validation Not Integrated with Fastify Schema

Current: Zod is used for manual .parse(request.body) calls inside route handlers. Fastify's native JSON Schema validation is not used. Query params and path params are cast with as type assertions — completely unvalidated.

Best Practice: Fastify is optimized for JSON Schema validation via schema option. Use fastify-type-provider-zod for both Zod DX and Fastify's compiled validation.

Fix: Adopt fastify-type-provider-zod to register Zod schemas in Fastify's schema option. At minimum, validate query and path params.

Medium Fragile Error Handling

Current: Global error handler detects ZodError by checking error.constructor.name === 'ZodError' (brittle, can break with minification). Some routes swallow errors and return 400 for all failures including 500-level DB errors. The /me endpoint returns errors without a proper HTTP status code.

Fix: Use instanceof ZodError. Differentiate client errors (400) from server errors (500) in catch blocks. Ensure all error paths set proper HTTP status codes.

Medium Rate Limiting Gaps

Current: Global rate limiting disabled (global: false). Only /login and /pin-login have rate limits (5 req/min).

Missing: /change-credential, /refresh token endpoint, and sync push endpoint have no rate limiting. No global fallback.

Fix: Add rate limiting to sensitive endpoints. Add global rate limit as safety net (e.g., 1000 req/min).

Low Plugin Registration Pattern

Current: The authenticate decorator is added to the root instance without fastify-plugin wrapping. Works but could break if plugin load order changes.

Fix: Wrap shared decorators with fastify-plugin for explicit encapsulation breaking.

Low Route Organization

Current: 22 route modules imported explicitly in app.ts. No grouping by domain.

Fix: Consider @fastify/autoload or group routes by domain (HQ management, POS operations, sync) into parent plugins.

Drizzle ORM & Database

Critical Missing Database Indexes

Current: Only unique indexes exist (from constraints). No non-unique indexes defined anywhere.

Critical missing indexes: sale_items.sale_id, sale_payments.sale_id, user_store_roles.user_id, sales.store_id, sales.created_at, sales.status, inventory_adjustments.product_id, products.department_id, products.sync_version, transfers.from_store_id, transfers.to_store_id, register_sessions.store_id, product_specials composite index.

Fix: Create a migration adding all missing indexes. Priority: FK columns used in joins, then filter columns, then sync_version columns.

Critical N+1 Queries in Sales List

Current: GET /api/v1/sales has a severe N+1 problem. For each sale, it queries items, then for each item it queries taxes, then queries payments. For a page of 50 sales with 5 items each: 50 (items) + 250 (taxes) + 50 (payments) = 350 queries per page load.

Fix: Collect all saleIds, then batch query: SELECT * FROM sale_items WHERE sale_id IN (...). Same for taxes and payments. Reduces 350 queries to 4.

High Sync Push Race Conditions (TOCTOU)

Current: processEnvelope() does NOT use transactions for multi-step operations. The shouldRemoteWin check-then-update pattern has a Time-of-Check-to-Time-of-Use race condition without a transaction.

Fix: Wrap sync push envelope processing in transactions. The select-then-upsert pattern needs to be atomic.

Medium No Drizzle Relations Defined

Current: No relations() defined anywhere. All joins are manual SQL-style innerJoin/leftJoin. No relational query API available.

Fix: Define Drizzle relations for key relationships (sale -> saleItems, product -> department, etc.). Use db.query relational API for reads needing eager loading.

Medium Connection Pool Not Configured

Current: Single postgres() connection with no pool configuration. No explicit pool size, no connection timeout, no idle timeout, no SSL for production.

Fix: Configure pool explicitly: postgres(url, { max: 20, idle_timeout: 30, connect_timeout: 10 }).

Low Query Pattern Inconsistency

Current: Products route constructs WHERE with manual SQL concatenation instead of and(...conditions) like sales route does correctly.

Fix: Standardize on and(...conditions) pattern everywhere.

Authentication & Authorization

High JWT Type Confusion

Current: Single JWT secret for all token types (access, refresh, store_sync). Access tokens don't include type: 'access'. The /me endpoint doesn't verify the token type. A refresh or sync token could be used as an access token.

Fix: Add type: 'access' to access tokens. Verify type in the authenticate decorator. Use separate secrets or iss/aud claims for different token types.

High Refresh Token Cannot Be Revoked

Current: Refresh token is a signed JWT with 7-day expiry. No server-side storage, no rotation, no family tracking. When a user is deactivated, their refresh tokens remain valid until natural expiry.

Fix: Store refresh tokens in DB. On refresh, invalidate old token and issue new one (rotation). Check user isActive on refresh.

High PIN Login Performance & Timing Attack

Current: PIN login iterates ALL active users and calls bcrypt.compare() against each until a match. With 100 users, worst case is ~10 second login (100 x ~100ms). Failed PINs always iterate all users; successful PINs stop early — timing attack vector. PIN uniqueness not enforced.

Fix: Enforce PIN uniqueness. Store a PIN hint or short hash for pre-filtering. Add constant-time comparison.

Medium Permission Loading Per Request

Current: requirePermission() loads all permissions from DB on every request (2-3 queries: userStoreRoles + rolePermissions JOIN permissions). Duplicated logic between auth-guard.ts and auth/routes.ts. No caching.

Fix: Consolidate permission loading. Cache permissions per userId with short TTL (60s) or include permissions in JWT.

WebSocket Server

Medium Sync Token in Query String

Current: WebSocket auth passes sync_token as a URL query parameter. Tokens in query strings are logged by reverse proxies and can leak via Referer headers.

Fix: Use the existing message-based auth flow as the primary mechanism. Remove query-string token support.

Low Heartbeat Timing

Current: 40s pong timeout with 30s ping interval gives only 10s grace period. Network jitter could cause false disconnects. Dual heartbeat mechanism (protocol-level ping from server, app-level heartbeat from store) is redundant.

Fix: Increase pong timeout to 60s or reduce ping interval to 15s.

Low No Horizontal Scaling

Current: All connected stores held in a single in-memory Map. Broadcast iterates all entries. No multi-process support.

Fix: Acceptable for current scale (<50 stores). For larger scale, introduce Redis pub/sub for cross-process routing.

Testing (Backend)

Test Coverage

HQ Server: 373 tests | Store Server: 185 tests

Framework: Vitest 3.x with global test setup, PostgreSQL test databases. Integration tests against real DB (no mocks). Global setup drops/recreates database, runs migrations, seeds data.

Medium Test Ordering Dependencies

Current: Tests within the same file share state (e.g., createdProductId set in POST test, used in PUT test). No per-test cleanup. Tests depend on execution order.

Fix: Use beforeEach for per-test data setup. Wrap tests in rolled-back transactions or don't depend on execution order.

Medium Missing Edge Case Tests

Current: Tests cover happy paths, 404s, and permission enforcement. Missing: concurrent operations, sync push conflict resolution, JWT type confusion, PIN-login timing with multiple users.

Fix: Add tests for JWT type validation, sync conflict resolution, concurrent sale creation.

Low Dead Sync-Processor Queue Code

Current: sync-processor queue defined but has no worker. Queue names are string literals. console.error used instead of Fastify logger.

Fix: Implement the sync-processor worker or remove dead code. Use app logger.

Frontend Analysis

Vue 3, Pinia, PrimeVue, Router, TypeScript, CSS

Vue 3 & Composition API

High Monolithic Views (1400+ lines)

Current: PosTerminalView.vue is 1400+ lines, ProductsView.vue is 900 lines, StoresView.vue is 586 lines. These combine data fetching, form handling, dialog management, and rendering in a single file.

Fix: Extract composables for data operations and split dialogs into child components. Target: no view exceeds ~300 lines of script.

High Tax Calculation Logic Duplicated

Current: The tax breakdown calculation in completeSale() (checkout store) is duplicated nearly verbatim in preValidateSale() (CheckoutView).

Fix: Extract the tax breakdown builder into a shared function in @pos/tax-engine.

Medium Duplicated formatMoney() Utility

Current: formatMoney(cents) is defined locally in ProductsView, CheckoutView, StoresView, StoreInventoryView independently.

Fix: Extract useFormatMoney() composable or utility in @pos/ui-kit.

Update (Mar 2026): The @pos/pos-logic package extraction consolidates shared stores and composables. Utility extraction is the next step.

Pinia Stores

High Duplicated Data-Unwrapping Logic

Current: Nearly every store has the same pattern: const payload = result.data as unknown as { data: T[] } | T[]; products.value = Array.isArray(payload) ? payload : ('data' in payload ? payload.data : []). This is repeated across all stores.

Fix: Handle response normalization once in the backend adapter's execute() method so stores never need to unwrap.

Update (Mar 2026): Partially addressed — 12 Pinia stores extracted to @pos/pos-logic and shared across all 4 apps, eliminating per-app store duplication.

Medium Stores Called at Setup-Time in Checkout

Current: useCheckoutStore calls useCartStore(), useAuthStore(), useRegisterStore() at the top level of the store definition. Pinia docs warn this can cause circular dependency issues.

Fix: Move store cross-references inside actions: const cartStore = useCartStore() inside completeSale().

Medium Reports Store Shares Single Loading Flag

Current: useReportsStore has 11 nearly identical async functions sharing one loading/error ref. Loading Report A clobbers Report B's loading state.

Fix: Split into per-report composables or add per-report loading state.

Medium Direct Ref Mutation in Product Store

Current: applySpecialsToProducts() mutates product.specialPrice directly on items inside products.value, circumventing Vue's reactivity tracking for nested properties.

Fix: Create new objects instead of mutating existing refs in place.

Low No Persistence Plugin

Current: Auth store manually implements localStorage persistence. Works but non-standard.

Fix: Consider pinia-plugin-persistedstate which handles edge cases (storage quota, serialization errors).

PrimeVue Components

High No Form Validation Anywhere

Current: No form validation library (Vee-Validate, FormKit, or manual validation). Users can submit empty product names, zero-price items, etc. PrimeVue 4's built-in invalid prop is not used.

Fix: Adopt Vee-Validate + Zod or at minimum use PrimeVue's :invalid prop with manual validation.

Medium Mixed Dropdown and Select Usage

Current: CheckoutView.vue still imports Dropdown (deprecated in PrimeVue 4). Other views correctly use Select.

Fix: Replace all Dropdown imports with Select.

Medium No Toast Service for Notifications

Current: All feedback is via inline Message components. Ephemeral success/error notifications should use PrimeVue's Toast service for better UX.

Fix: Add PrimeVue ToastService and use useToast() for success/error notifications.

Medium Inline Styles in Templates

Current: Multiple views use inline style="font-size:0.8125rem;...". Should use CSS classes for consistency and dark-mode safety.

Fix: Replace inline styles with scoped CSS classes.

Low Legacy TabView/TabPanel API

Current: PrimeVue 4 introduced Tabs/TabList/Tab/TabPanels API. The older TabView/TabPanel API is still used in ProductsView.

Router & Navigation

Medium Auth Guard Uses Fragile $subscribe Polling

Current: The beforeEach guard waits for authStore.initializing with a manual $subscribe + Promise pattern. Fragile.

Fix: Replace with authStore.whenReady() that returns a cached promise.

Medium No Route-Level Error Handling

Current: No onError handler on the router. If a lazy-loaded chunk fails to load (network error), the user sees nothing.

Fix: Add router.onError() with a retry/reload mechanism.

Low No Scroll Behavior Configured

Current: createRouter does not set scrollBehavior. Scroll position not reset on navigation.

Fix: Add scrollBehavior: () => ({ top: 0 }) to router config.

TypeScript & CSS

Critical Massive Type Duplication

Current: ProductsView.vue defines its own Product, Department, TaxGroup, Supplier, Store interfaces nearly identical to @pos/shared-types. Same in CheckoutView, StoreInventoryView, etc. Every view re-defines types that already exist.

Fix: Import from @pos/shared-types instead of re-defining interfaces in views.

Critical Excessive as unknown as Casts

Current: Multiple stores use result.data as unknown as { data: T[] } | T[] to handle inconsistent API response shapes. Bypasses type safety entirely. Root cause: backend adapter's response unwrapping is incomplete.

Fix: Fix the backend adapter to consistently unwrap responses, eliminating all as unknown as casts.

Medium any Usage in StoresView

Current: StoresView.vue line 125 uses .map((s: any) => ({...})). Violates the project's "no any" rule.

Fix: Replace with proper typed parameter.

Medium Mixed Responsive Direction

Current: CheckoutView uses mobile-first (min-width breakpoints), while ProductsView and StoreInventoryView use desktop-first (max-width).

Fix: Standardize on mobile-first (recommended for POS targeting tablets).

Medium Hardcoded Colors Instead of Tokens

Current: StoresView uses #10b981, #ef4444 directly instead of PrimeVue tokens. Will not adapt to dark mode correctly.

Fix: Use var(--p-green-500), var(--p-red-500) PrimeVue CSS variables.

i18n Compliance

Critical Hardcoded English Strings

Current: Multiple views contain hardcoded English text in violation of the project's "All user-facing text via vue-i18n" rule. Affected: StoresView ("Stores Dashboard", "Add Store", "Connected", etc.), StoreInventoryView ("Inventory", "Search products...", "Low Stock Only", etc.), LoginView ("PIN / Password"), and others.

Fix: Audit all views for hardcoded strings. Add keys to both en.json and es.json.

Medium No Type-Safe Translation Keys

Current: Translation keys are plain strings. Typos in t('products.tilte') silently fail at runtime, showing the key instead of translated text.

Fix: Generate TypeScript types from en.json keys for type-safe t() calls.

Backend Adapter

High Fragile String-Based Command Mapping

Current: commandToEndpoint has 100+ entries mapping command strings to URLs. commandToMethod has 30+ if-chains. commandToSuffix has 30+ entries. No compile-time validation; typos silently fall through.

Fix: Replace with a typed registry object: { get_products: { method: 'GET', path: '/api/v1/products', paginated: true } }.

High Incomplete Response Normalization

Current: The web adapter partially unwraps { success, data } responses but doesn't handle paginated responses consistently. Every store must re-unwrap with as unknown as casts.

Fix: Have the adapter fully normalize responses so stores receive clean typed data.

Medium No Request Interceptors

Current: No way to add global error handling (401 -> logout), request logging, or retry logic.

Fix: Add an interceptor chain for global 401 redirect and retry.

Build & Bundling

Medium Service Worker Caches Financial API Responses

Current: Workbox config caches /api/ responses for 5 minutes. For a POS system handling financial transactions, stale API cache can lead to incorrect stock levels, prices, or duplicate sales.

Fix: Remove or restrict Workbox API caching to safe read-only endpoints only.

Medium No Manual Chunk Splitting

Current: PrimeVue is a large library. Without manualChunks, the entire PrimeVue code may end up in one vendor chunk.

Fix: Add manualChunks to split PrimeVue, Chart.js, and vue-i18n into separate vendor chunks.

Medium Turbo Test Depends on Full Build

Current: In turbo.json, "test": { "dependsOn": ["build"] } means every test run requires a full build, slowing feedback.

Fix: Change to "dependsOn": ["^build"] so tests only require dependency packages to be built.

Testing (Frontend)

Test Coverage

Web Client: 248 tests | Packages: 211 tests

Framework: Vitest 3.x + @vue/test-utils + happy-dom

Low No ESLint Configuration

Current: The root package.json has a lint script but no ESLint configuration exists. No @typescript-eslint or eslint-plugin-vue.

Fix: Add ESLint with @typescript-eslint and eslint-plugin-vue.

Sync & Data Flow

WebSocket notifications + HTTP data sync with transactional outbox

Critical Sync Version Tracking is Fundamentally Broken

Current: Per-row syncVersion integer counter, NOT a global monotonically increasing sequence. Multiple rows can share the same syncVersion value. Store pulls with since_version=N and gets rows where syncVersion > N. If Product A and Product B both have syncVersion=5, a pull with since_version=5 returns nothing — both products are missed.

Additionally: Some entity types (permissions, role_permissions, user_store_roles) use pullAll() which re-downloads the entire table every 30-second sync cycle regardless of changes.

Fix: Create a global PostgreSQL sequence: CREATE SEQUENCE sync_global_seq START 1; Set syncVersion = nextval('sync_global_seq') on every INSERT/UPDATE. Add syncVersion to tables that currently lack it.

Critical Conflict Resolution Bug: createdAt vs updatedAt

Current: shouldRemoteWin() compares existing.createdAt against remoteTimestamp. This means a record created early but updated recently could be overwritten by an older remote change. This is a bug.

Fix: Change shouldRemoteWin() to compare against updatedAt instead of createdAt. Add a conflict log table recording overwritten values for audit.

Current Sync Architecture

Pattern: WebSocket notification → HTTP pull/push
Outbox: Transactional outbox pattern for store → HQ pushes
Entities: 21 entity types (products, departments, taxes, tax groups, tenders, users, roles, specials, customers, sales reps, store config, etc.)
Direction: Bidirectional — HQ pushes master data down, stores push sales/inventory up
Polling: 30-second interval, 21 sequential HTTP requests per store per cycle

Pull Mechanism

High Inefficient Pull: 21 Sequential HTTP Requests Per Cycle

Current: Each pull cycle iterates through 21 entity types sequentially, making 21 separate HTTP requests. At 20 stores x 30s intervals = 420 HTTP requests/minute to HQ. The latestVersion calculation loops through all returned rows in JavaScript instead of SQL MAX().

Fix: Consolidate into a single POST /api/v1/sync/pull accepting all entity versions. Add LIMIT and cursor pagination for large result sets. Use SQL MAX().

Push Mechanism

High Push Has No Idempotency / Deduplication

Current: No idempotency key on push envelopes. If a push succeeds but the store's outbox deletion fails (e.g., DB crash after HTTP 200), the same entry is pushed again and HQ re-processes it, potentially double-decrementing inventory.

Fix: Add an idempotency key (outbox entry UUID) to push envelopes. HQ should track processed IDs and skip duplicates.

High No Transactional Batch Processing

Current: Each processEnvelope() call is a separate DB operation. A sale and its sale_items/sale_payments could be partially synced. Sale items may arrive before their parent sale, causing FK violations. Pull of 21 entity types sequentially means point-in-time inconsistency.

Fix: Wrap batch processing in a DB transaction. Group related entities (sale + sale_items + sale_payments) as atomic units. Enforce parent-before-child ordering.

High Outbox Not Truly Transactional

Current: addToOutbox() takes db as a parameter but there is no guarantee the caller wraps the business operation and outbox insert in a single transaction. If the sale INSERT succeeds but the outbox INSERT fails, data is inconsistent.

Fix: Verify all call sites wrap business operation + outbox insert in the same transaction.

Data Flow Lifecycles

Product Lifecycle

Flow: HQ creates product (syncVersion=1) → Store pulls via GET /sync/pull?entity_type=products → HQ creates store_product_dynamic row during pull → Store upserts product locally → POS sells product, inserts into sync_outbox → Push to HQ → HQ adjusts quantity on store_product_dynamic.

Risk: If pull fails after products inserted locally but before dynamic row created on HQ, store has products without HQ-side tracking. If same sale is re-pushed (idempotency failure), quantity is double-decremented.

Customer Lifecycle (Bidirectional)

Flow: Store A creates customer locally → Push to HQ → HQ upserts, increments syncVersion → Store B pulls new customer.

Risk: Two stores creating the same customer simultaneously creates duplicates (UUID-based upsert, no natural key dedup). Customer updates at both stores in the same sync window: LWW resolves by timestamp, loser's changes silently dropped.

Transfer Lifecycle

Flow: Store A creates transfer (pending) → Push to HQ → HQ notifies Store B via WebSocket → Store B pulls, receives, adjusts inventory → Push back to HQ.

Risk: No state machine enforcement — any status can be written by any store. Store B may try to receive before pulling the transfer.

Offline Behavior

Info Offline Operation Works Correctly

The store server operates independently with its own PostgreSQL. Sales, inventory, and register sessions work against the local DB. The outbox queues changes for later push. WebSocket reconnects with exponential backoff. Architecture supports indefinite offline operation (10,000 sales = ~20MB outbox).

Medium No Catch-Up Prioritization After Reconnect

Current: After a long offline period, the store pulls all 21 entity types sequentially. Critical data (prices, tax rates) should be prioritized. No delta compression. Both push and pull fire simultaneously, potentially saturating the link.

Fix: Prioritize critical entity types. Add compression. Stagger push/pull.

Medium Outbox Overflow — No Backpressure

Current: No size limit on outbox. A busy store offline for 3 days accumulates ~12,000 entries. Push sends 100 at a time every 15 seconds, so clearing backlog takes ~30 minutes. No compression, no dynamic batch sizing, no monitoring.

Fix: Add dynamic batch sizing, gzip compression, outbox size monitoring with alerts.

Failure Recovery

Medium Dead Letter Management

Current: Outbox entries with attempts >= 10 are logged as dead letters but remain in the table indefinitely. No cleanup, no alerting, no management UI.

Fix: Move dead letters to a separate table. Add API endpoint to list/retry/purge. Add automated alerting when count exceeds threshold.

Security Analysis

Authentication, secrets management, data protection, access control

Severity Finding Affected Area Recommendation
Critical Same JWT secret shared across HQ and all store servers. A compromised store token works everywhere. HQ + Store servers, docker-compose.prod.yml Use separate JWT secrets per service, or add iss/aud claims and validate them.
Critical Secrets in plaintext in docker-compose.prod.yml: JWT secret (pos-production-jwt-secret-change-me), DB passwords, sync tokens. docker-compose.prod.yml Use Docker secrets, .env file excluded from Git, or a secrets manager (Vault, Coolify secrets).
High CORS defaults to origin: true (allow ALL origins) when CORS_ORIGINS env var is not set. HQ + Store app.ts Set explicit CORS origins: CORS_ORIGINS=https://hq.pos.eddieadiaz.com,https://tienda1.pos.eddieadiaz.com
High JWT type confusion: access tokens lack type: 'access'. Refresh and sync tokens could be used as access tokens. Auth routes, authenticate decorator Add type: 'access' to access tokens. Verify type in authenticate decorator.
High Refresh tokens cannot be revoked. No server-side storage, no rotation, no family tracking. Auth routes Store refresh tokens in DB. Implement token rotation on refresh.
High PIN login O(N) bcrypt comparisons. 100 users = ~10s login. Timing attack: failed PINs iterate all users, successful stop early. Auth routes (pin-login) Enforce PIN uniqueness. Add PIN hint/short hash for pre-filtering. Constant-time comparison.
High No automated PostgreSQL backups. Named volumes with no backup strategy. docker-compose.prod.yml Implement pg_dump cron + offsite copy. Add backup verification.
High No monitoring, metrics, or alerting. Logging is console.log/console.error only. All servers Add Prometheus metrics, health check endpoints, structured JSON logging (Pino), log aggregation.
Medium Only login routes have rate limiting. All other endpoints unprotected. HQ + Store servers Enable global rate limiting (100 req/min per IP). Stricter limits on mutation endpoints.
Medium Store nginx confs missing security headers. HQ has X-Frame-Options, X-Content-Type-Options, Referrer-Policy but stores do not. deploy/store1/nginx.conf, deploy/store2/nginx.conf Copy security headers from HQ nginx to all store nginx confs.
Medium WebSocket sync_token passed as URL query parameter. Logged by reverse proxies, visible in server logs. Sync WebSocket connection Use message-based auth as primary. Remove query-string token support.
Medium All Docker services share a single internal network. Store 1 DB accessible from Store 2 server. Redis has no password. docker-compose.prod.yml Create per-store networks. Add Redis password. Network segmentation.
Medium No Content-Security-Policy header. No X-XSS-Protection on store configs. Nginx configs Add CSP headers in nginx. SQL injection prevented by Drizzle; XSS by Vue; but defense-in-depth missing.
Low No account lockout. Rate limiting is per-IP, not per-user. Distributed attacker can try from multiple IPs. Auth routes Add per-user lockout after N failed attempts.
Low Input validation adequate (Drizzle parameterized queries + Vue escaping) but no explicit string sanitization. All API routes Defense-in-depth: add CSP headers, consider input length limits.

Scale Analysis

Performance considerations, bottlenecks, growth capacity

Baseline: 20 Stores, 10K Products, 30s Sync Interval

420
HTTP req/30s (current pull)
20
HTTP req/30s (batch pull)
~17
Push req/min (steady)
Pull Load Analysis

Current: 20 stores x 21 entity types = 420 HTTP requests per 30 seconds = 14 req/sec to HQ. Most return 0 entities but each still runs a DB query. 420 PostgreSQL queries/30s for pull alone.

With batch pull (recommended): 20 requests per 30 seconds = 0.67 req/sec. Each runs ~21 queries but in a single round-trip.

Bandwidth Estimation

Initial sync (10K products): ~10K rows x ~500 bytes = 5MB. Should be paginated.

Incremental (steady state): ~10-50 changed entities per cycle, <50KB. Negligible.

Push (100 sale_items): ~100 x 300 bytes = 30KB per batch. Negligible.

Medium Connection Pooling Not Configured

Current: postgres(config.databaseUrl) with default pool (10 connections). No explicit pool size, no monitoring. HQ handles 20 store sync connections + HQ manager clients + API calls. Could exceed default pool.

Fix: Configure explicit pool size: postgres(url, { max: 20 }). Add PgBouncer in transaction mode for HQ. Monitor pool utilization.

Scaling Beyond 20 Stores

At 100 Stores

Current architecture: 2100 pull HTTP requests per 30 seconds = 70 req/sec. This stresses a single Node.js process. Would need:

  • Batch pull endpoint (reduces to 100 req/30s = 3.3 req/sec)
  • Read replicas for pull queries
  • Load balancer if single HQ server is insufficient
  • WebSocket moved to dedicated process using Redis pub/sub

Docker Deployment

Medium No Application Health Checks

Current: PostgreSQL and Redis have health checks. Application containers (hq-server, store-server) have NO health checks. Docker cannot detect if Node.js is unhealthy (event loop blocked, DB connection lost).

Fix: Add health check: test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]

Medium No Resilience Patterns

Current: No connection retry on startup. No circuit breaker for DB operations. Pull requests from stores return 500 on HQ DB failure, triggering immediate retry with no backoff.

Fix: Add DB connection retry on startup (exponential backoff). Add circuit breaker for sync endpoints. Store should back off when HQ returns 5xx.

Low Zero-Downtime Deployment Not Supported

Current: deploy.sh runs docker compose up -d --force-recreate with 5-10 seconds downtime per deploy.

Fix: Acceptable for off-hours deploys. For 24/7, use Traefik health-check-based routing or blue-green.

Tauri Desktop App

Tauri 2 with Rust backend for POS Terminal, HQ Manager, Store Manager. All Tauri apps now import shared logic from @pos/pos-logic.

App Status Overview

POS Terminal
80%

17 Tauri commands implemented. Full sales, register, sync, receipt printing (TCP). USB printer stubbed.

Store Manager
55%

13 commands: inventory, transfers, reports, sync. No hardware integration. No auth commands.

HQ Manager
5%

Pure skeleton. No commands, no DB connection, no business logic. Only Tauri shell.

POS Terminal Details

Implemented Features

Auth: PIN/password login with bcrypt verification against local DB
Products: get_products, get_product_by_barcode with SQL queries
Sales: complete_sale (transactional, inventory deduction, sync outbox), get_sale, get_recent_sales, void_sale
Register: open_register, close_register, get_current_session, get_session_summary
Hardware: Full ESC/POS command builder, receipt rendering, paper cut, barcode printing (TCP only). Cash drawer via ESC/POS pulse.
Sync: Push outbox, pull 9 entity types, WebSocket with reconnect, fallback timer (60s)
Window: Fullscreen, 1280x800 (kiosk mode)

Code Quality Observations

Medium String-Based Error Types

Current: All Rust code uses String for error types via .map_err(|e| format!(...)) rather than proper thiserror enums, despite thiserror being in dependencies.

Fix: Define thiserror enums for each domain (DbError, AuthError, SyncError, HardwareError).

Medium Single DB Connection, Not Pool

Current: Database access uses a single Arc<Mutex<Option<Client>>>. All DB operations are serialized through one connection.

Fix: Use deadpool-postgres or bb8-postgres for a connection pool.

Low Manual Transaction Management

Current: Uses manual BEGIN/COMMIT/ROLLBACK strings. The unwrap_or(false) in bcrypt verification masks potential errors.

Fix: Use a structured transaction guard. Handle bcrypt errors explicitly.

Info Not Yet Compiled or Tested

None of the three Tauri apps have been compiled in the current environment. They exist as committed source code but no Cargo.lock or target/ directories are present.

Architecture Recommendations

POS Terminal: YES, build with Tauri + local DB

Must operate offline (network outages). Hardware integration (printer, drawer, scanner) requires native OS access. Kiosk mode is standard. Local DB queries are sub-millisecond. The existing implementation is 80% complete. Estimated effort to production: 4-6 weeks.

Store Manager: OPTIONAL — web-only sufficient for v1

Store managers work from back-office with reliable network. No hardware integration needed. The web-client already covers all functionality. Add Tauri later only if offline inventory management or native report export is required.

HQ Manager: NO — use web-client exclusively

HQ operations always require network. No hardware needed. Web-client covers all HQ functionality. Updates via web deployment are simpler than desktop updates. The existing Tauri app is a skeleton with zero implementation. Recommend: abandon.

Hardware Integration Status

Hardware Status Technology Notes
Receipt Printer (TCP) Done Custom ESC/POS implementation Full command set, receipt rendering, configurable
Receipt Printer (USB) Stubbed rusb crate needed Returns "not yet implemented". Add via rusb 0.9+
Cash Drawer Done ESC/POS pulse via printer Sends ESC p 0 25 250 through printer connection
Barcode Scanner Done Keyboard wedge (HID) Vue useBarcodeScanner.ts composable handles input
Customer Display Future Second Tauri window or serial Tauri v2 supports multi-window

All Findings

Sortable table of all analysis findings across the system

# Area Severity Finding Best Practice Fix
1 Sync Critical Per-row sync version counter, not global sequence — stores miss updates Global monotonic sequence (PostgreSQL SEQUENCE) Create sync_global_seq, assign on every write
2 Sync Critical shouldRemoteWin() compares createdAt not updatedAt Compare updatedAt for LWW conflict resolution Fix comparison field, add conflict audit log
3 Security Critical Same JWT secret shared across HQ and all store servers Separate secrets per service or iss/aud claims Use separate JWT secrets, add iss/aud validation
4 Security Critical Secrets (JWT, DB passwords, sync tokens) in plaintext in docker-compose.prod.yml Docker secrets, .env excluded from Git, or Vault Move all secrets to Docker secrets or .env
5 Database Critical Missing indexes on FK columns (sale_items.sale_id, user_store_roles.user_id, sales.store_id, etc.) Index all FK columns used in joins and filters Create migration adding ~20 missing indexes
6 Database Critical N+1 queries in sales list — 350 queries per page load Batch queries with IN (...) clauses Collect IDs, batch fetch items/taxes/payments
7 Frontend Critical Hardcoded English strings in multiple views (i18n violation) All user-facing text via vue-i18n Audit all views, add keys to en.json + es.json
8 TypeScript Critical Views re-define interfaces from @pos/shared-types locally Import from shared-types package Replace local interfaces with shared-types imports
9 TypeScript Critical Excessive as unknown as casts due to inconsistent API response shape Backend adapter should normalize responses Fix adapter to unwrap consistently
10 Sync Critical Sync push processEnvelope() not wrapped in transactions (TOCTOU race) Atomic check-then-update in transactions Wrap envelope processing in DB transactions
11 Security High CORS defaults to origin: true (allow ALL origins) Explicit allowed origins list Set CORS_ORIGINS env var in production
12 Auth High JWT type confusion — refresh/sync tokens accepted as access tokens Add type claim, verify in authenticate decorator Add type: 'access' to access tokens, validate
13 Auth High Refresh tokens cannot be revoked (no server-side storage) Store refresh tokens in DB with rotation Add refresh token table, implement rotation
14 Auth High PIN login O(N) bcrypt — 100 users = ~10s, timing attack vector PIN uniqueness, pre-filter hash, constant-time Enforce PIN uniqueness, add lookup optimization
15 Sync High 21 sequential HTTP requests per store per pull cycle Single batch pull endpoint Consolidate into POST /sync/pull with all versions
16 Sync High Push has no idempotency — re-push causes duplicate processing Idempotency keys, tracked processed IDs Add outbox UUID as idempotency key on HQ
17 Sync High No transactional batch processing, no FK ordering Atomic batch processing, parent-before-child Wrap batches in transactions, enforce ordering
18 Types High Shared types don't match actual sync wire format Single source of truth (Zod schema) Align SyncEntityType with actual values
19 Deploy High No automated PostgreSQL backup strategy pg_dump cron + offsite copy + verification Implement automated backup pipeline
20 Ops High No monitoring, metrics, alerting, or structured logging Prometheus, Pino structured logging, alerting Add health endpoints, metrics, log aggregation
21 Frontend High Monolithic views (PosTerminalView 1400+ lines, ProductsView 900 lines) Max ~300 lines per view, extract composables Decompose into sub-components and composables
22 Frontend High No form validation anywhere in the application Vee-Validate + Zod or PrimeVue :invalid prop Add validation library, enforce on all forms
23 Pinia High Duplicated data-unwrapping logic in every store Normalize once in backend adapter Fix adapter execute() to fully unwrap responses
24 Adapter High Fragile string-based command-to-endpoint mapping (100+ entries) Typed registry object with compile-time validation Replace with typed command definitions
25 Adapter High Incomplete response normalization in web adapter Adapter handles all unwrapping, stores get clean data Fully normalize responses including pagination
26 Vue High Tax calculation duplicated between checkout store and CheckoutView Extract to @pos/tax-engine shared function Single tax breakdown builder in shared package
27 Fastify Medium Zod validation not integrated with Fastify schema system Use fastify-type-provider-zod Register Zod schemas in Fastify schema option
28 Fastify Medium Fragile error handling (constructor name check, missing status codes) instanceof checks, proper HTTP status codes Use instanceof ZodError, differentiate 400/500
29 Fastify Medium Rate limiting only on login endpoints Global rate limit + per-endpoint limits Add limits to /change-credential, /refresh, sync
30 Drizzle Medium No Drizzle relations defined — manual joins everywhere Define relations() for relational query API Add relations for key relationships
31 Database Medium Connection pool not configured (using defaults) Explicit pool config with monitoring Set max: 20, idle_timeout: 30, connect_timeout: 10
32 Auth Medium Permission loading: 2-3 DB queries per authenticated request, no cache Cache with short TTL or include in JWT Add per-userId cache with 60s TTL
33 WebSocket Medium Sync token in URL query string (logged by proxies) Message-based auth only Use first-message auth, remove query string
34 Testing Medium Tests within files depend on execution order Independent tests with per-test setup Use beforeEach for data setup
35 Testing Medium Missing edge case tests (concurrency, JWT confusion, sync conflicts) Test critical security and sync paths Add JWT type, sync conflict, concurrency tests
36 Pinia Medium Stores called at setup-time in checkout (circular dep risk) Call store references inside actions Move useCartStore() etc. inside action functions
37 Pinia Medium Reports store shares single loading flag across 11 reports Per-report loading state Split into composables or add per-report flags
38 PrimeVue Medium Deprecated Dropdown component still used in CheckoutView Use Select (PrimeVue 4 replacement) Replace Dropdown imports with Select
39 PrimeVue Medium No Toast service for ephemeral notifications Use PrimeVue ToastService for success/error Add ToastService, use useToast() composable
40 Router Medium Auth guard uses fragile $subscribe polling pattern authStore.whenReady() cached promise Replace $subscribe with promise-based ready check
41 Router Medium No router.onError() handler for failed chunk loads Error handler with retry/reload Add router.onError() with user-facing retry
42 TypeScript Medium any usage in StoresView (violates project rules) Strict TypeScript, no any Replace with proper typed parameter
43 i18n Medium No type-safe translation keys — typos fail silently Generate TypeScript types from en.json keys Add typed keys via vue-i18n generics
44 CSS Medium Mixed mobile-first and desktop-first responsive breakpoints Consistent direction (mobile-first recommended) Standardize on min-width breakpoints
45 CSS Medium Hardcoded hex colors instead of PrimeVue CSS variable tokens Use var(--p-green-500) etc. Replace #hex colors with PrimeVue tokens
46 Build Medium Service worker caches financial API responses for 5 minutes No caching for transactional endpoints Remove or restrict to read-only endpoints
47 Build Medium No manual chunk splitting for PrimeVue/Chart.js manualChunks in rollup config Split PrimeVue, Chart.js, vue-i18n into chunks
48 Monorepo Medium Test task depends on full build in turbo.json Depend on ^build (only dependency packages) Change dependsOn to ["^build"]
49 Vue Medium Duplicated formatMoney() utility across 4 views Shared utility in @pos/ui-kit Extract useFormatMoney() composable
50 Adapter Medium No request interceptors for global 401 handling Interceptor chain for auth redirect, retry Add middleware/interceptor support to adapter
51 Security Medium Store nginx confs missing security headers Match HQ nginx security headers Copy X-Frame-Options etc. to store configs
52 Deploy Medium All stores share single Docker network (no segmentation) Per-store networks with limited cross-links Create store1-net, store2-net, add Redis password
53 Deploy Medium No application health checks in Docker containers HTTP health check endpoint Add /health endpoint and Docker healthcheck
54 Sync Medium permissions/roles re-downloaded every 30s cycle (pullAll) Add syncVersion to these tables Incremental sync for all entity types
55 Sync Medium Dead letter entries accumulate forever, no alerts Separate table, management API, alerting Move dead letters, add API + webhook alerts
56 Sync Medium Outbox overflow: no size limits, no adaptive batching Dynamic batch sizing, compression, monitoring Add gzip, dynamic batches, outbox alerts
57 Types Medium API response not typed as discriminated union { success: true; data: T } | { success: false; error } Change ApiResponse to discriminated union
58 Tauri Medium Rust error types are Strings, not thiserror enums Proper error enums with thiserror Define domain-specific error enums
59 Tauri Medium Single DB connection (Arc Mutex), not a pool Connection pool (deadpool-postgres) Replace with connection pool crate
60 Fastify Low Plugin registration without fastify-plugin wrapping Wrap shared decorators with fastify-plugin Add fastify-plugin for authenticate decorator
61 Fastify Low 22 explicit route imports, no grouping by domain @fastify/autoload or domain grouping Consider autoload or parent plugin grouping
62 Drizzle Low Inconsistent query patterns (manual SQL vs and()) Standardize on and(...conditions) Refactor products route query builder
63 Queue Low Dead sync-processor queue code, console.error logging Remove dead code, use Fastify logger Implement worker or remove queue definition
64 WebSocket Low Heartbeat timing: 10s grace period, dual mechanism Increase pong timeout to 60s Adjust timing or reduce ping interval
65 Pinia Low No persistence plugin for auth store Use pinia-plugin-persistedstate Consider adopting persistence plugin
66 Pinia Low Sync store event listener never cleaned up Call dispose() on store teardown Wire up dispose in SPA lifecycle
67 Router Low No scroll behavior configured in router scrollBehavior: () => ({ top: 0 }) Add scrollBehavior to createRouter
68 Router Low Some store routes missing explicit permissions meta Explicit permissions on all routes Add meta.permissions to store routes
69 Monorepo Low No ESLint configuration in the project @typescript-eslint + eslint-plugin-vue Add ESLint config with Vue and TS rules
70 Build Low No build size analysis tooling rollup-plugin-visualizer Add bundle analyzer to track size growth
71 Deploy Low No zero-downtime deployment (5-10s gap per deploy) Blue-green or Traefik health-check routing Acceptable for off-hours; plan for 24/7 later
72 Tauri Low Manual BEGIN/COMMIT/ROLLBACK, bcrypt error masking Structured transaction guard Use transaction helper, explicit error handling

Fix Roadmap

Prioritized timeline for addressing findings

Phase 1: Critical Security & Data Integrity (Week 1)
1. Fix shouldRemoteWin() to compare updatedAt not createdAt (1 line fix, critical data integrity bug).
2. Move secrets out of docker-compose.prod.yml into .env or Docker secrets.
3. Set unique JWT secrets per service (or add iss/aud claims).
4. Set explicit CORS origins in production environment.
Findings: #2, #3, #4, #11 | Estimated: 2-3 days
Phase 2: Sync Protocol Redesign (Weeks 1-2)
5. Implement global sync sequence (replace per-row version with atomic counter).
6. Wrap sync push processEnvelope() in database transactions.
7. Add push idempotency (track processed envelope UUIDs on HQ).
8. Add batch pull endpoint (single request for all entity types).
9. Add syncVersion to permissions/roles tables for incremental sync.
Findings: #1, #10, #15, #16, #17, #54 | Estimated: 1-2 weeks
Phase 3: Database Performance (Week 2)
10. Create migration adding all missing database indexes (~20 indexes).
11. Fix N+1 query in sales list endpoint (batch queries, 350 -> 4).
12. Configure connection pool explicitly (max: 20, timeouts).
Findings: #5, #6, #31 | Estimated: 2-3 days
Phase 4: Auth & Security Hardening (Week 3)
13. Add type: 'access' to access tokens, verify in authenticate decorator.
14. Store refresh tokens in DB with rotation.
15. Optimize PIN login (enforce uniqueness, pre-filter hash).
16. Add rate limiting to sensitive endpoints.
17. Copy security headers to store nginx configs.
18. Add Redis password, per-store Docker networks.
Findings: #12-14, #29, #51, #52 | Estimated: 1 week
Phase 5: Type Safety & Frontend Correctness (Weeks 3-4)
19. Fix backend adapter to fully normalize API responses (eliminates #9, #23, #25).
20. Replace local interfaces with @pos/shared-types imports.
21. Align shared types with actual sync wire format.
22. Audit all views for hardcoded English strings, add i18n keys.
23. Remove all as unknown as casts from stores.
24. Fix any usage in StoresView.
Findings: #7-9, #18, #42 | Estimated: 3-5 days
Phase 6: Infrastructure & Ops (Week 4)
25. Implement automated PostgreSQL backup (pg_dump cron + offsite copy).
26. Add application health check endpoints and Docker healthchecks.
27. Add Prometheus metrics, structured JSON logging (Pino), alerting.
28. Add dead letter management (separate table, API, alerts).
Findings: #19, #20, #53, #55 | Estimated: 1 week
Phase 7: Component Architecture & UX (Weeks 5-6)
29. Decompose monolithic views into sub-components (<300 lines each).
30. Extract shared formatMoney() utility and tax calculation helper.
31. Add form validation (Vee-Validate + Zod).
32. Add PrimeVue Toast service, replace deprecated Dropdown with Select.
33. Move store cross-references inside Pinia actions.
Findings: #21-22, #26, #36, #38-39, #49 | Estimated: 1-2 weeks
Phase 8: Build & DX Improvements (Week 6)
34. Standardize responsive breakpoints (mobile-first).
35. Replace hardcoded hex colors with PrimeVue tokens.
36. Configure manual chunk splitting for PrimeVue/Chart.js.
37. Remove API response caching from service worker.
38. Fix turbo.json test dependency, add ESLint.
39. Add router error handler, scroll behavior.
Findings: #41, #44-48, #67, #69 | Estimated: 3-5 days
Phase 9: Tauri POS Terminal to Production (Weeks 7-10)
40. Compile POS Terminal, fix build issues, connect Vue frontend to Tauri commands.
41. Test with real PostgreSQL and hardware (printer, drawer, scanner).
42. Add USB printer support via rusb crate.
43. Add tauri-plugin-updater for auto-updates.
44. Build CI/CD pipeline for installer generation.
45. Optionally: migrate POS Terminal from PostgreSQL to SQLite.
Tauri analysis | Estimated: 4-6 weeks
Phase 10: Long-Term Architecture (Quarter 2+)
46. Evaluate CDC (Debezium) for replacing polling-based sync.
47. Plan store-local deployment architecture.
48. Add OpenAPI spec generation.
49. Consider CRDTs for bidirectional entities (customers).
50. Implement rolling deploys for zero-downtime updates.
Long-term roadmap | Ongoing