Technical Architecture
Technical Architecture
HCP is built on modern web technologies optimised for mobile field use.
Tech Stack
Backend
| Component | Technology | Version |
|---|---|---|
| Framework | Ruby on Rails | 8.1.1 |
| Language | Ruby | 3.3+ |
| Database | SQLite (dev), PostgreSQL (prod) | 3.x / 14+ |
| Authentication | has_secure_password | Built-in |
| Authorization | Pundit | 2.3+ |
| Jobs | Solid Queue | Built-in |
| Caching | Solid Cache | Built-in |
Frontend
| Component | Technology |
|---|---|
| Framework | Hotwire (Turbo + Stimulus) |
| Styling | Tailwind CSS v4 |
| Theme | Shad-CN with OKLCH colors |
| Scanning | @zxing/browser |
| Icons | Lucide |
Infrastructure
| Component | Technology |
|---|---|
| Container | Docker |
| Deployment | Kamal |
| Proxy | Thruster |
| Server | Puma |
Data Model
Core Entities
# Organisation — Top-level containerclass Organisation has_many :clients has_many :users has_many :sites, through: :clientsend
# Client — Retail chain/brandclass Client belongs_to :organisation has_many :sites has_many :productsend
# Site — Individual retail locationclass Site belongs_to :client has_many :site_products has_many :products, through: :site_products has_many :site_memberships has_many :users, through: :site_membershipsend
# Product — Item in catalogclass Product belongs_to :client has_many :site_products has_many :sites, through: :site_productsend
# SiteProduct — Join with thresholdsclass SiteProduct belongs_to :site belongs_to :product has_many :count_entries has_many :sap_entries has_many :anomaliesendEntry Models
# Physical count entryclass CountEntry belongs_to :site_product belongs_to :user
# Fields: # - date # - locker_opening, locker_stock_in, locker_shelf_out, locker_damages # - sales_floor_opening, sales_floor_shelf_in, sales_floor_damages
after_save :evaluate_anomaliesend
# SAP data entryclass SapEntry belongs_to :site_product belongs_to :user
# Fields: # - date # - inventory_qty # - sales_qty # - notes
after_save :evaluate_anomaliesend
# Auto-detected issueclass Anomaly belongs_to :site_product belongs_to :source, polymorphic: true # CountEntry or SapEntry
# Fields: # - detected_on (date) # - rule (enum: locker_short, sales_floor_short, etc.) # - severity (ok, watch, alert) # - value, thresholdendAnomaly Detection System
AnomalyEvaluator Service
class AnomalyEvaluator RULES = %i[ locker_short sales_floor_short out_of_stock damages_high short_delivery osa_breach on_shelf_available ]
def evaluate(site_product, date) # Destroy existing anomalies for date # Evaluate each rule # Create new anomalies for triggered rules # Upsert to maintain current state endendRule Logic
| Rule | Condition | Severity |
|---|---|---|
| locker_short | locker_closing < 0 | ALERT |
| sales_floor_short | sales_floor_closing < 0 | ALERT |
| out_of_stock | sales_floor_closing == 0 | ALERT |
| damages_high | damages > threshold | ALERT |
| short_delivery | delivery < threshold % | WATCH |
| osa_breach | (sales_floor / total) < target % | WATCH |
| on_shelf_available | sales_floor > 0 | OK |
Authentication Flow
User enters email + PIN ↓SessionsController#create ↓User.authenticate(pin) [bcrypt] ↓Session created (cookie) ↓Redirect to role-appropriate landingPIN Security
- 4-digit PINs stored with bcrypt
- Rate limiting: 5 attempts per 20 seconds
- Session timeout: 30 minutes
Authorization (Pundit)
Policy Structure
class ApplicationPolicy def initialize(user, record) @user = user @record = record endend
class SitePolicy < ApplicationPolicy def show? user.admin? || user.supervisor? || assigned_to_user? end
def update? user.admin? || user.manager? endendRole Hierarchy
class User def super_admin?; role == 'super_admin'; end def admin?; role == 'admin'; end def manager?; role == 'manager'; end def supervisor?; role == 'supervisor'; end def controller?; role == 'controller'; endendFrontend Architecture
Stimulus Controllers
// Real-time balance calculations
// scan_checklist_controller.js// Barcode scanning and modal managementMobile Optimisations
- Viewport —
viewport-fit=coverfor notched devices - Touch targets — Min 44x44px for buttons
- Input modes —
inputmode="numeric"triggers number pad - Camera — Full-width preview with overlay
Deployment
Docker Production
# Multi-stage buildFROM ruby:3.3-slim AS builder# ... gems, assets
FROM ruby:3.3-slim AS runtime# ... runtime onlyEXPOSE 80CMD ["./bin/thrust", "./bin/rails", "server"]Kamal Configuration
service: hcpimage: benmacdonald/hcpservers: web: - 192.168.1.1proxy: ssl: true host: hcp.example.comAPI Endpoints
Barcode Lookup
GET /barcode/lookup?barcode=1234567890
Response:{ "item": { "name": "Product Name", "sku": "SKU123", "barcode": "1234567890", "category": "Electronics" }}Next: Deployment Guide