A secure, full-stack Virtual POS system built with React, Node.js/Express, and PostgreSQL, featuring Dynamic Currency Conversion (DCC), Idempotency protection, Stripe integration, Webhook support, and enterprise-grade network architecture including SOCKS5 tunneling, transparent proxy, and mobile VPOS reverse proxy authentication.
| Layer | Technology |
|---|---|
| Frontend | React 18, React Router v6 |
| Backend | Node.js 20, Express 4 |
| Database | PostgreSQL 16 |
| Payments | Stripe |
| BIN Lookup | binlist.net (free) / bintable.com (paid) |
| FX Rates | frankfurter.app (free) / exchangeratesapi.io (paid) |
| Proxy | HTTP / HTTPS / SOCKS5 |
| Reverse Proxy | Nginx |
| Containerization | Docker + Docker Compose |
- Node.js 20+
- PostgreSQL 16+
- npm
cd vpos
# Backend
cp backend/.env.example backend/.env
# Edit backend/.env with your values# Frontend
cp frontend/.env.example frontend/.env# Backendcd backend
npm install
# Frontendcd ../frontend
npm installcd backend
# Run migrations (creates all tables)
npm run migrate
# Seed default admin user
npm run seed
# Or do both at once
npm run setupcd backend
npm run dev
# Runs on http://localhost:5000cd frontend
npm start
# Runs on http://localhost:3000# Check backend health (includes proxy and tunnel status)
curl http://localhost:5000/api/healthThe VPOS supports four enterprise network modes that can be combined:
┌─────────────────────────────────────────────────────┐
│ VPOS Terminals │
│ (Desktops / Tablets / Mobile) │
└──────────────────────┬──────────────────────────────┘
│
┌──────────▼──────────┐
│ VPOS Backend │
│ (Node.js/Express) │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Proxy Layer │
│ HTTP/HTTPS/SOCKS5 │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Payment Gateway │
│ (Stripe API) │
└─────────────────────┘
Used when VPOS is on an air-gapped network with no direct internet access. The proxy acts as the bridge between the internal network and Stripe.
Setup in backend/.env:
OUTBOUND_PROXY_URL=http://your-proxy-server:8080# Or with authentication:OUTBOUND_PROXY_URL=http://username:password@your-proxy-server:8080What it does:
- All Stripe API calls route through your proxy
- Your proxy's static IP is what Stripe sees (use for IP whitelisting)
- Full packet inspection by your security stack
- Centralized logging of all payment traffic
Best for organizations needing a VPN-like tunnel without a full VPN. SOCKS5 handles any protocol (TCP/UDP) on any port — not just HTTP.
Setup in backend/.env:
# Without authenticationOUTBOUND_PROXY_URL=socks5://your-proxy-server:1080# With authenticationOUTBOUND_PROXY_URL=socks5://username:password@your-proxy-server:1080What it does:
- All payment traffic tunneled through SOCKS5
- Protocol agnostic — works with any payment processor
- All terminals appear as one static IP to Stripe
- Identity masking for multi-terminal deployments
Setting up a SOCKS5 server (Linux):
# Install Dante SOCKS5 server
apt-get install dante-server
# Edit /etc/danted.conf
logoutput: /var/log/danted.log
internal: eth0 port = 1080
external: eth0
method: username none
user.privileged: root
user.unprivileged: nobody
# Start service
systemctl start danted
systemctl enable dantedThe most VPN-like setup. VPOS terminals require zero configuration. Your router intercepts all traffic on port 443 and redirects to the proxy.
Setup in backend/.env:
TRANSPARENT_PROXY_MODE=trueRouter configuration (iptables on Linux gateway):
# Redirect all HTTPS traffic to proxy (port 3128)
iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 3128
# Save rules
iptables-save > /etc/iptables/rules.v4Squid proxy config (/etc/squid/squid.conf):
http_port 3128 intercept
https_port 3129 intercept ssl-bump cert=/etc/squid/ssl_cert/myCA.pem
ssl_bump server-first all
acl payment_gateways dstdomain .stripe.com
http_access allow payment_gateways
http_access deny all
What it does:
- Zero config on VPOS terminals
- Router handles all interception
- Employees cannot bypass or tamper with settings
- All traffic logged at network level
For mobile units like delivery agents or field sales reps. Each device must authenticate with a unique token before processing payments.
Step 1 — Enable mobile mode in backend/.env:
MOBILE_VPOS_MODE=trueStep 2 — Register each device in backend/.env:
# Format: DEVICE_TOKEN_<DEVICEID>=<secret-token># Device IDs must be uppercase, no spacesDEVICE_TOKEN_TABLET001=aB3xK9mP2qR7sT1uV4wY6zDEVICE_TOKEN_TABLET002=cD5eF8gH0iJ2kL4mN6oP8qDEVICE_TOKEN_MOBILE001=rS1tU3vW5xY7zA9bC2dE4fStep 3 — Configure each mobile device to send these headers with every request: X-Device-Id: TABLET001 X-Device-Token: aB3xK9mP2qR7sT1uV4wY6z
Step 4 — Revoke a stolen device instantly:
If a tablet is stolen, remove its token from .env and restart the backend.
That device can no longer process any payments immediately.
Generating secure device tokens:
# Linux/Mac
openssl rand -hex 32
# PowerShell (Windows)
[System.Web.Security.Membership]::GeneratePassword(32, 8)
# Or simpler:
-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 32|% {[char]$_})Only approved domains can receive outbound connections. All other connections are blocked and logged.
Setup in backend/.env:
ALLOWED_OUTBOUND_DOMAINS=api.stripe.com,lookup.binlist.net,api.frankfurter.app,api.exchangeratesapi.io,api.bintable.comOnce your proxy is set up with a static IP:
- Go to dashboard.stripe.com
- Settings → Security → Allowed IP addresses
- Add your proxy server's public IP
- All terminals now share one trusted identity
- Manual entry: Card number, Expiry, CVV
- Real-time Luhn algorithm validation
- Card brand detection (Visa, Mastercard, Amex, Discover, JCB)
- Animated card preview
- CVV masking with correct length per brand
- BIN lookup detects card issuing country
- Offers conversion to customer's home currency
- Live exchange rates (1-hour DB cache)
- 2.5% DCC markup (industry standard)
- Full audit trail per transaction
- Auto-generated per charge session
- Duplicate clicks never double-charge
- 24-hour TTL with automatic cleanup
- Signature verification on every event
- Handles: succeeded, failed, refunded, disputed
- Duplicate event protection
- Full event log in database
users— merchant accountstransactions— full charge historyidempotency_keys— deduplicationexchange_rate_cache— FX cacheaudit_log— all actions with IPwebhook_events— delivery log
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/login | Login |
| GET | /api/auth/profile | Get profile |
| PUT | /api/auth/change-password | Change password |
| Method | Endpoint | Headers | Description |
|---|---|---|---|
| POST | /api/payments/dcc-quote | Bearer | DCC quote |
| POST | /api/payments/charge | Bearer + Idempotency-Key | Process charge |
| GET | /api/payments/transactions | Bearer | List transactions |
| GET | /api/payments/transactions/:id | Bearer | Single transaction |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health | Health + proxy + tunnel status |
| POST | /api/webhooks/stripe | Stripe events |
vpos/
├── backend/
│ ├── src/
│ │ ├── controllers/
│ │ │ ├── authController.js
│ │ │ └── paymentController.js
│ │ ├── middleware/
│ │ │ ├── auth.js
│ │ │ ├── domainWhitelist.js
│ │ │ ├── idempotency.js
│ │ │ └── sanitize.js
│ │ ├── migrations/
│ │ │ ├── run.js
│ │ │ └── seed.js
│ │ ├── models/
│ │ │ └── db.js
│ │ ├── routes/
│ │ │ ├── auth.js
│ │ │ └── payment.js
│ │ ├── services/
│ │ │ ├── currencyService.js
│ │ │ ├── proxyConfig.js
│ │ │ └── tunnelService.js
│ │ ├── utils/
│ │ │ └── logger.js
│ │ ├── webhooks/
│ │ │ └── stripeWebhook.js
│ │ └── index.js
│ ├── Dockerfile
│ ├── package.json
│ └── .env.example
│
├── frontend/
│ ├── public/
│ │ └── index.html
│ ├── src/
│ │ ├── components/
│ │ │ └── Layout.js
│ │ ├── pages/
│ │ │ ├── Charge.js
│ │ │ ├── Transactions.js
│ │ │ └── Settings.js
│ │ ├── styles/
│ │ │ └── global.css
│ │ ├── utils/
│ │ │ ├── api.js
│ │ │ └── cardUtils.js
│ │ ├── App.js
│ │ └── index.js
│ ├── Dockerfile
│ ├── package.json
│ └── .env.example
│
├── nginx/
│ └── nginx.conf
├── docker-compose.yml
├── .env.example
└── README.md
- Set strong
JWT_SECRET(openssl rand -hex 64) - Set strong
DB_PASSWORD - Add real Stripe live secret key
- Configure Stripe webhook endpoint and secret
- Add SSL certificate to
nginx/ssl/ - Update
ALLOWED_ORIGINSwith your domain - Configure outbound proxy (
OUTBOUND_PROXY_URL) - Add proxy static IP to Stripe IP whitelist
- Set
ALLOWED_OUTBOUND_DOMAINSfor PCI-DSS - Register all mobile devices with unique tokens
- Enable
TRANSPARENT_PROXY_MODEif using router interception - Set up PostgreSQL backups
- Configure log rotation for
backend/logs/ - Set
NODE_ENV=production
Proprietary — for internal organisational use only.