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
| Component | Purpose |
|---|---|
DataTable | Sortable, searchable, paginated, selectable table with CSV export |
SearchBar | Debounced text input for filtering data |
Pagination | Page navigation with configurable page sizes |
AlertBadge | Visual indicator for low-stock or pending items |
BulkActions | Multi-select action bar (delete, export, status change) |
UndoButton | Time-limited undo for destructive actions |
Toast | Non-blocking notification system (success, error, info) |
ConfirmDialog | Modal confirmation before critical actions |
PasswordInput | Input 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
| Route | Component | Access |
|---|---|---|
/login | LoginPage | Public |
/forgot-password | ForgotPasswordPage | Public |
/ | DashboardPage | All authenticated |
/inventory | InventoryPage | Admin, Production |
/purchases | PurchasesPage | Admin, Production |
/production | ProductionPage | Admin, Production |
/sales | SalesPage | Admin, Sales |
/customers | CustomersPage | Admin, Sales |
/accounting | AccountingPage | Admin, Accountant |
/users | UsersPage | Admin only |
/requisitions | RequisitionsPage | All authenticated |
/reports | ReportsPage | Admin, Accountant |
/settings | SettingsPage | Admin 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
- Client sends HTTP request to Nginx (port 443/8443)
- Nginx terminates SSL and forwards to Express (port 8443 internal)
- Express runs middleware chain (CORS → Parser → Logger → Auth → RBAC)
- Route Handler executes business logic, queries MySQL
- MySQL returns result set
- Route Handler formats JSON response
- Express sends response back through Nginx to client
- 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
| Table | Purpose | Key Relationships |
|---|---|---|
users | System users with roles and credentials | Referenced by all tables via created_by |
settings | Key-value system configuration | Standalone |
inventory | Stock items (oils, materials, finished goods) | Referenced by inventory_adjustments, purchases |
purchases | Purchase orders from suppliers | FK to suppliers, users |
production_runs | Manufacturing/production records with BOM | FK to users |
sales | Sales orders to customers | FK to customers, users |
expenses | Financial expense records | FK to users |
inventory_adjustments | Stock adjustment audit trail | FK to inventory, users |
ar_invoices | Accounts receivable invoices | FK to sales |
ar_payments | AR payment receipts | FK to ar_invoices, users |
requisitions | Multi-level approval workflow items | FK to users (requester + approvers) |
customers | Customer master data | Referenced by sales, ar_invoices |
suppliers | Supplier master data | Referenced by purchases |
audit_log | Immutable system audit trail | FK 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
| Component | Detail |
|---|---|
| Provider | Oracle Cloud Infrastructure |
| Tier | Always Free |
| Instance | ARM-based Ampere A1 |
| CPU | Up to 4 OCPUs |
| RAM | Up to 24 GB |
| Storage | Up to 200 GB boot volume |
| Public IP | 170.9.14.60 |
| OS | Ubuntu (or Oracle Linux) |
5.2 Nginx Configuration
Nginx serves dual roles:
- Reverse Proxy: Forwards API requests to the Express backend
- 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 pathsproxy_pass http://127.0.0.1:8443: Forward API trafficroot /var/www/.../dist: Serve React buildtry_files $uri $uri/ /index.html: SPA fallback for client-side routinggzip 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
| Record | Type | Value |
|---|---|---|
loftygoldenoil.com | A | 170.9.14.60 |
www.loftygoldenoil.com | CNAME | loftygoldenoil.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
| Aspect | Staging | Production |
|---|---|---|
| URL | http://170.9.14.60:8443 | https://loftygoldenoil.com |
| SSL | None (HTTP) | Let's Encrypt (HTTPS) |
| Domain | IP-based | Domain-based |
| Purpose | Testing, QA, demo | Live business operations |
| Database | Same server, separate DB or shared | Same server |
| Deploy Trigger | Manual or feature branch | Push to main |
7. Security Architecture
7.1 Authentication & Authorisation
| Mechanism | Implementation |
|---|---|
| Password Storage | bcryptjs (salt rounds: 10) |
| Session Management | JWT tokens (stateless, expiry: 24h configurable) |
| Token Transmission | HTTP Authorization header (Bearer scheme) |
| Role-Based Access | 4 roles: Admin, Production, Sales, Accountant |
| Route Protection | Auth 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 / Action | Admin | Production | Sales | Accountant |
|---|---|---|---|---|
| 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
| Aspect | Detail |
|---|---|
| Provider | Resend (HTTP API) |
| Integration | Direct HTTP POST requests from Express |
| Templates | 7 branded HTML email templates |
| Templates List | Welcome, 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