Skip to main content

System Architecture — Lofty Golden Oil ERP

1. Architecture Overview

Lofty Golden Oil ERP follows a classic three-tier architecture: a presentation tier (React SPA), an application tier (Express REST API), and a data tier (MySQL 8.0). The system is deployed on a single Oracle Cloud Free Tier VPS with Nginx serving as a reverse proxy and static file server, and PM2 managing the Node.js process lifecycle.


2. Frontend Architecture

2.1 Technology

  • Framework: React 19 (Functional Components + Hooks)
  • Build Tool: Vite 6 (fast HMR, optimized production builds)
  • HTTP Client: Axios (configured with base URL and interceptors)
  • Routing: React Router v6 (client-side SPA routing)
  • State Management: React Context API + useState/useReducer hooks

2.2 Component Tree

App
├── AuthProvider (Context — JWT token, user info, login/logout)
│ ├── ThemeProvider (Context — dark/light mode)
│ │ ├── ToastProvider (Context — toast notifications)
│ │ │ ├── ConfirmDialogProvider (Context — modal confirmations)
│ │ │ │ ├── AppRouter (React Router)
│ │ │ │ │ ├── Public Routes
│ │ │ │ │ │ ├── LoginPage
│ │ │ │ │ │ └── ForgotPasswordPage
│ │ │ │ │ └── Protected Routes (requires valid JWT)
│ │ │ │ │ ├── Layout (Sidebar + Header + Main)
│ │ │ │ │ │ ├── DashboardPage
│ │ │ │ │ │ ├── InventoryPage
│ │ │ │ │ │ │ ├── InventoryList
│ │ │ │ │ │ │ ├── InventoryForm (Create/Edit)
│ │ │ │ │ │ │ └── InventoryAdjustmentModal
│ │ │ │ │ │ ├── PurchasesPage
│ │ │ │ │ │ ├── ProductionPage
│ │ │ │ │ │ ├── SalesPage
│ │ │ │ │ │ ├── CustomersPage
│ │ │ │ │ │ ├── AccountingPage
│ │ │ │ │ │ ├── UsersPage
│ │ │ │ │ │ ├── RequisitionsPage
│ │ │ │ │ │ ├── ReportsPage
│ │ │ │ │ │ └── SettingsPage
│ │ │ │ │ └── NotFoundPage

2.3 Shared Components

ComponentPurpose
DataTableSortable, searchable, paginated, selectable table with CSV export
SearchBarDebounced text input for filtering data
PaginationPage navigation with configurable page sizes
AlertBadgeVisual indicator for low-stock or pending items
BulkActionsMulti-select action bar (delete, export, status change)
UndoButtonTime-limited undo for destructive actions
ToastNon-blocking notification system (success, error, info)
ConfirmDialogModal confirmation before critical actions
PasswordInputInput with show/hide toggle for secure password entry

2.4 State Management Pattern

Global State (Context)
├── AuthContext → user, token, login(), logout(), isAuthenticated
├── ThemeContext → theme (dark/light), toggleTheme()
├── ToastContext → toasts[], showToast(message, type)
└── ConfirmContext → showConfirm(title, message) → Promise<boolean>

Local State (useState/useReducer)
├── Page-level data (fetched on mount via useEffect)
├── Form state (controlled components)
├── Filter/search state
└── Modal/drawer visibility

2.5 Routing

RouteComponentAccess
/loginLoginPagePublic
/forgot-passwordForgotPasswordPagePublic
/DashboardPageAll authenticated
/inventoryInventoryPageAdmin, Production
/purchasesPurchasesPageAdmin, Production
/productionProductionPageAdmin, Production
/salesSalesPageAdmin, Sales
/customersCustomersPageAdmin, Sales
/accountingAccountingPageAdmin, Accountant
/usersUsersPageAdmin only
/requisitionsRequisitionsPageAll authenticated
/reportsReportsPageAdmin, Accountant
/settingsSettingsPageAdmin only

3. Backend Architecture

3.1 Technology

  • Runtime: Node.js (LTS)
  • Framework: Express 5
  • Database Driver: mysql2 (with connection pooling)
  • Authentication: JSON Web Tokens (JWT) via jsonwebtoken
  • Password Hashing: bcryptjs
  • Email: Resend HTTP API
  • File Upload/Export: CSV generation via custom utilities

3.2 Middleware Stack

Incoming Request

├── 1. CORS Middleware
│ └── Configured allowed origins (production domain + staging IP)

├── 2. Body Parser (express.json)
│ └── Parses JSON request bodies (max 10MB)

├── 3. Request Logger
│ └── Logs method, URL, timestamp, user IP

├── 4. Rate Limiter (conceptual)
│ └── Prevents brute-force and DDoS patterns

├── 5. Auth Middleware (JWT Verify)
│ └── Extracts Bearer token → verifies signature → attaches user to req
│ └── Skipped for public routes (login, forgot-password)

├── 6. Role-Based Access Control (RBAC) Middleware
│ └── Checks req.user.role against route permission matrix
│ └── Returns 403 Forbidden if role not permitted

├── 7. Route Handler
│ └── Business logic (controller)
│ └── Database queries via MySQL2 pool
│ └── Response JSON (success or error)

└── 8. Error Handler Middleware
└── Catches unhandled errors
└── Returns structured JSON error response
└── Logs error to console/audit_log

3.3 JWT Authentication Flow

┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │ │ Server │ │ MySQL │ │ Resend │
│ (Browser) │ │ (Express) │ │ (DB) │ │ (Email) │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ POST /api/auth/login │ │
│ {email, password} ──────────────────►│ │
│ │ SELECT user │
│ │ WHERE email = ? │
│ ◄────────────────────│
│ │ user row │
│ ├───────────────────►│
│ │ bcrypt.compare() │
│ │ password match? │
│ ◄───────────────────│
│ │ │
│ │ jwt.sign(payload, │
│ │ secret, {expiresIn})
│ {token, user} ◄─────────────────────│ │
│ │ │
│ GET /api/inventory │ │
│ Authorization: Bearer <token> ──────► │
│ │ jwt.verify() │
│ │ decode user │
│ │ check role (RBAC) │
│ │ SELECT inventory │
│ [inventory data] ◄──────────────────│ │

3.4 Request Lifecycle

  1. Client sends HTTP request to Nginx (port 443/8443)
  2. Nginx terminates SSL and forwards to Express (port 8443 internal)
  3. Express runs middleware chain (CORS → Parser → Logger → Auth → RBAC)
  4. Route Handler executes business logic, queries MySQL
  5. MySQL returns result set
  6. Route Handler formats JSON response
  7. Express sends response back through Nginx to client
  8. Client updates React state and re-renders UI

3.5 API Route Structure

/api/
├── auth/
│ ├── POST /login — Authenticate user, return JWT
│ ├── POST /forgot-password — Send password reset email
│ └── POST /reset-password — Reset password with token
├── users/
│ ├── GET / — List all users (admin)
│ ├── POST / — Create user (admin)
│ ├── PUT /:id — Update user (admin)
│ └── DELETE /:id — Delete user (admin)
├── inventory/
│ ├── GET / — List inventory items
│ ├── POST / — Create inventory item
│ ├── PUT /:id — Update inventory item
│ ├── DELETE /:id — Delete inventory item
│ └── POST /adjust — Create stock adjustment
├── purchases/
│ ├── GET / — List purchases
│ ├── POST / — Create purchase
│ ├── PUT /:id — Update purchase
│ └── DELETE /:id — Delete purchase
├── production/
│ ├── GET / — List production runs
│ ├── POST / — Create production run
│ ├── PUT /:id — Update production run
│ └── DELETE /:id — Delete production run
├── sales/
│ ├── GET / — List sales
│ ├── POST / — Create sale
│ ├── PUT /:id — Update sale
│ └── DELETE /:id — Delete sale
├── customers/
│ ├── GET / — List customers
│ ├── POST / — Create customer
│ ├── PUT /:id — Update customer
│ └── DELETE /:id — Delete customer
├── suppliers/
│ ├── GET / — List suppliers
│ ├── POST / — Create supplier
│ ├── PUT /:id — Update supplier
│ └── DELETE /:id — Delete supplier
├── accounting/
│ ├── GET /gl — General ledger entries
│ ├── POST /gl — Create GL entry
│ ├── GET /trial-balance — Trial balance report
│ ├── GET /profit-loss — P&L statement
│ ├── GET /balance-sheet — Balance sheet
│ ├── GET /bank-reconciliation— Bank reconciliation
│ ├── POST /journal-entries — Create journal entry
│ └── GET /chart-of-accounts — Chart of accounts
├── expenses/
│ ├── GET / — List expenses
│ ├── POST / — Create expense
│ ├── PUT /:id — Update expense
│ └── DELETE /:id — Delete expense
├── ar/
│ ├── GET /invoices — List AR invoices
│ ├── POST /invoices — Create AR invoice
│ ├── POST /payments — Record AR payment
│ └── GET /statements/:id — Customer statement
├── requisitions/
│ ├── GET / — List requisitions
│ ├── POST / — Create requisition
│ ├── PUT /:id/approve — Approve requisition
│ ├── PUT /:id/reject — Reject requisition
│ └── GET /:id — Get requisition detail
├── reports/
│ └── GET /consolidated — Consolidated report
├── audit/
│ └── GET / — Audit log entries
└── settings/
├── GET / — Get system settings
└── PUT / — Update system settings

4. Database Architecture

4.1 Database Engine

  • Engine: MySQL 8.0
  • Charset: utf8mb4 (full Unicode support)
  • Connection Pooling: mysql2 pool (configurable min/max connections)

4.2 Entity Relationship — 14 Core Tables

┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ users │ │ suppliers │ │ customers │
│──────────────│ │──────────────│ │──────────────│
│ id (PK) │ │ id (PK) │ │ id (PK) │
│ email │ │ name │ │ name │
│ password_hash│ │ contact │ │ contact │
│ role │ │ phone │ │ phone │
│ full_name │ │ address │ │ address │
│ is_active │ │ created_at │ │ credit_limit │
│ created_at │ └──────────────┘ │ balance │
│ updated_at │ │ created_at │
└──────────────┘ └──────────────┘
│ │
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ settings │ │ inventory │ │ sales │
│──────────────│ │──────────────│ │──────────────│
│ id (PK) │ │ id (PK) │ │ id (PK) │
│ key │ │ name │ │ customer_id │──►customers
│ value │ │ description │ │ date │
│ updated_by │ │ quantity │ │ total_amount │
│ updated_at │ │ unit │ │ status │
└──────────────┘ │ unit_price │ │ created_by │──►users
│ min_stock │ │ created_at │
│ category │ └──────────────┘
│ created_by │──►users │
│ created_at │ │
└──────────────┘ │
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ purchases │ │ inventory │ │ ar_invoices │
│──────────────│ │ _adjustments │ │──────────────│
│ id (PK) │ │──────────────│ │ id (PK) │
│ supplier_id │──► │ id (PK) │ │ sale_id │──►sales
│ item_name │ │ inventory_id │──► │ invoice_no │
│ quantity │ │ adjustment │ │ amount │
│ unit_price │ │ reason │ │ due_date │
│ total_cost │ │ adjusted_by │──► │ status │
│ date │ │ created_at │ │ created_at │
│ created_by │──► └──────────────┘ └──────────────┘
│ created_at │ │
└──────────────┘ ▼
┌──────────────┐ ┌──────────────┐
┌──────────────┐ │ production │ │ ar_payments │
│ expenses │ │ _runs │ │──────────────│
│──────────────│ │──────────────│ │ id (PK) │
│ id (PK) │ │ id (PK) │ │ invoice_id │──►ar_invoices
│ category │ │ product_name │ │ amount │
│ description │ │ bom │ │ payment_date │
│ amount │ │ quantity │ │ method │
│ date │ │ yield_pct │ │ reference │
│ created_by │──► │ cost_per_litre│ │ created_by │──►users
│ created_at │ │ status │ │ created_at │
└──────────────┘ │ start_date │ └──────────────┘
│ end_date │
│ created_by │──►users
│ created_at │
└──────────────┘

┌──────────────┐ ┌──────────────┐
│ requisitions │ │ audit_log │
│──────────────│ │──────────────│
│ id (PK) │ │ id (PK) │
│ requester_id │──► │ user_id │──►users
│ type │ │ action │
│ description │ │ entity_type │
│ amount │ │ entity_id │
│ status │ │ details │
│ level_1 │ │ ip_address │
│ level_2 │ │ created_at │
│ level_3 │ └──────────────┘
│ level_4 │
│ created_by │──►users
│ created_at │
└──────────────┘

4.3 Table Descriptions

TablePurposeKey Relationships
usersSystem users with roles and credentialsReferenced by all tables via created_by
settingsKey-value system configurationStandalone
inventoryStock items (oils, materials, finished goods)Referenced by inventory_adjustments, purchases
purchasesPurchase orders from suppliersFK to suppliers, users
production_runsManufacturing/production records with BOMFK to users
salesSales orders to customersFK to customers, users
expensesFinancial expense recordsFK to users
inventory_adjustmentsStock adjustment audit trailFK to inventory, users
ar_invoicesAccounts receivable invoicesFK to sales
ar_paymentsAR payment receiptsFK to ar_invoices, users
requisitionsMulti-level approval workflow itemsFK to users (requester + approvers)
customersCustomer master dataReferenced by sales, ar_invoices
suppliersSupplier master dataReferenced by purchases
audit_logImmutable system audit trailFK to users

4.4 Connection Pool Configuration

const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'erp_user',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'lofty_golden_oil',
port: process.env.DB_PORT || 3306,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});

5. Infrastructure Architecture

5.1 Server Specification

ComponentDetail
ProviderOracle Cloud Infrastructure
TierAlways Free
InstanceARM-based Ampere A1
CPUUp to 4 OCPUs
RAMUp to 24 GB
StorageUp to 200 GB boot volume
Public IP170.9.14.60
OSUbuntu (or Oracle Linux)

5.2 Nginx Configuration

Nginx serves dual roles:

  1. Reverse Proxy: Forwards API requests to the Express backend
  2. Static File Server: Serves the built React SPA
Client Request (HTTPS:443)


┌─────────────────────────┐
│ NGINX │
│ │
│ Is the path /api/*? │
│ ├─ YES → proxy_pass │──► http://localhost:8443 (Express)
│ │ (API routes) │
│ └─ NO → serve static │──► /var/www/lofty-golden-oil/dist/
│ (React SPA) │
└─────────────────────────┘

Key Nginx Configuration Directives:

  • ssl_certificate / ssl_certificate_key: Let's Encrypt paths
  • proxy_pass http://127.0.0.1:8443: Forward API traffic
  • root /var/www/.../dist: Serve React build
  • try_files $uri $uri/ /index.html: SPA fallback for client-side routing
  • gzip on: Enable compression for static assets

5.3 PM2 Process Management

# Start the application
pm2 start server.js --name "lofty-golden-oil-api"

# View status
pm2 status

# View logs
pm2 logs lofty-golden-oil-api

# Restart after deployment
pm2 reload lofty-golden-oil-api

# Auto-start on system reboot
pm2 startup
pm2 save

PM2 Benefits:

  • Automatic restart on crash or unhandled exceptions
  • Log rotation and management
  • Cluster mode for multi-core CPU utilisation
  • Process monitoring dashboard

5.4 SSL/TLS

  • Provider: Let's Encrypt (free, automated)
  • Renewal: Certbot with cron job (every 60 days, 30-day validity)
  • Certificate Type: Domain-validated (DV)
  • Protocol: TLS 1.2+
  • Cipher Suite: Modern, secure defaults
# Initial certificate request
certbot --nginx -d loftygoldenoil.com -d www.loftygoldenoil.com

# Auto-renewal (cron)
0 0 1 * * certbot renew --quiet

5.5 DNS

RecordTypeValue
loftygoldenoil.comA170.9.14.60
www.loftygoldenoil.comCNAMEloftygoldenoil.com

6. Deployment Architecture

6.1 CI/CD Pipeline

┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ GitHub │ │ GitHub │ │ VPS │ │ PM2 │
│ Push │───►│ Actions │───►│ (SCP) │───►│ Reload │
│ (main) │ │ (CI/CD) │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘

Step 1: Developer pushes to main branch
Step 2: GitHub Actions workflow triggers
Step 3: Workflow executes:
a. Install dependencies (npm ci)
b. Run linting/type-checking
c. Run tests (if configured)
d. Build frontend (npm run build)
Step 4: SCP deployment:
a. Copy backend files to VPS via SSH key
b. Copy built frontend to /var/www/ directory
Step 5: PM2 reload:
a. pm2 reload lofty-golden-oil-api
b. Verify process health
Step 6: (Optional) Nginx reload if config changed

6.2 GitHub Actions Workflow

name: Deploy to Production

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Deploy via SCP
uses: appleboy/scp-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
source: "server/,dist/,package.json,package-lock.json,ecosystem.config.js"
target: ${{ secrets.DEPLOY_PATH }}
- name: Reload PM2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: cd ${{ secrets.DEPLOY_PATH }} && pm2 reload lofty-golden-oil-api

6.3 Staging vs Production

AspectStagingProduction
URLhttp://170.9.14.60:8443https://loftygoldenoil.com
SSLNone (HTTP)Let's Encrypt (HTTPS)
DomainIP-basedDomain-based
PurposeTesting, QA, demoLive business operations
DatabaseSame server, separate DB or sharedSame server
Deploy TriggerManual or feature branchPush to main

7. Security Architecture

7.1 Authentication & Authorisation

MechanismImplementation
Password Storagebcryptjs (salt rounds: 10)
Session ManagementJWT tokens (stateless, expiry: 24h configurable)
Token TransmissionHTTP Authorization header (Bearer scheme)
Role-Based Access4 roles: Admin, Production, Sales, Accountant
Route ProtectionAuth middleware + RBAC middleware per route

7.2 Security Layers

Layer 1: Network
├── Oracle Cloud security lists (firewall rules)
├── Nginx rate limiting (conceptual)
└── SSL/TLS encryption (HTTPS)

Layer 2: Application
├── CORS policy (allowed origins)
├── JWT verification (every protected request)
├── RBAC middleware (role checking)
├── Input validation/sanitisation
└── SQL injection prevention (parameterised queries via mysql2)

Layer 3: Data
├── bcrypt password hashing
├── Parameterised SQL queries
├── Database user with limited privileges
└── Audit logging (immutable trail)

7.3 Role-Permission Matrix

Route / ActionAdminProductionSalesAccountant
Dashboard
Inventory CRUD
Inventory Adjustments
Purchases
Production Runs
Sales CRUD
Customers CRUD
AR Invoices & Payments
Accounting (GL, TB, P&L)
Expenses
User Management
System Settings
Requisitions
Audit Log
Reports

7.4 CORS Configuration

const corsOptions = {
origin: [
'https://loftygoldenoil.com',
'http://170.9.14.60:8443'
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
};

8. Email Architecture

AspectDetail
ProviderResend (HTTP API)
IntegrationDirect HTTP POST requests from Express
Templates7 branded HTML email templates
Templates ListWelcome, Password Reset, Invoice, Statement, Monthly Report, Low Stock Alert, Requisition Alert

Email Trigger Points:

  • Welcome email → New user creation
  • Password reset email → Forgot password request
  • Invoice email → AR invoice creation
  • Statement email → Customer statement generation
  • Monthly report email → Scheduled or on-demand
  • Low stock alert → Inventory below threshold
  • Requisition alert → Requisition status change

Document version: 1.0 — July 2026