The Beginning: Why Vanilla?
In an era dominated by frameworks—Laravel, Symfony, React, Vue—I made what many would consider a controversial decision: build a fully-featured trading platform using vanilla PHP and vanilla JavaScript. No Laravel magic, no React complexity, no npm dependency hell. Just pure, unadulterated code.
Was I crazy? Maybe. But by the end of this journey, I had created Tradity, a progressive web application that streams live cryptocurrency prices, executes trades in real-time, and feels as snappy as any framework-powered platform—all while maintaining complete control over every line of code.
This is the story of how simplicity conquered complexity, and why sometimes, the old ways are the best ways.
Chapter 1: The Architecture Decision
The Problem
I wanted to build a stock/crypto trading simulator that could:
- Stream real-time price data from Binance
- Handle concurrent user connections without breaking a sweat
- Work offline with PWA capabilities
- Be deployable anywhere—from shared hosting to VPS
- Remain maintainable by a single developer (me!)
The Framework Temptation
Every tutorial screamed: "Use Laravel! Use Next.js! Use [insert framework here]!"
But frameworks come with baggage:
- Vendor lock-in: What happens when Laravel 15 breaks your Laravel 10 code?
- Complexity overhead: Do I really need 50MB of node_modules for a trading app?
- Learning curve: Framework conventions vs. fundamental programming
- Performance: Abstraction layers add latency—critical in trading
The Vanilla Revelation
Then it hit me: PHP is already a framework. It has routing ($_SERVER['REQUEST_URI']), templating (<?php echo $data ?>), and database access (mysqli, PDO). JavaScript has fetch API, ES6 modules, and native WebSocket support.
Decision made: Vanilla stack it is.
Chapter 2: Building the Backend—PHP's Hidden Power
The API Router (No Laravel Required)
// index.php - The heart of the backend
$request = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
// Simple, explicit routing
if ($request === '/api/auth/login' && $method === 'POST') {
AuthController::login();
} elseif ($request === '/api/trades' && $method === 'GET') {
TradeController::getTradeHistory();
} else {
Response::error('Endpoint not found', 404);
}
Why this is beautiful:
- ✅ Zero routing overhead
- ✅ Explicit control flow
- ✅ Easy debugging (no magic middleware chains)
- ✅ Works on any PHP 7.4+ server
The Service Layer Pattern
I adopted a clean architecture approach:
Controllers → Services → Models
Example: Executing a Trade
// TradeController.php
class TradeController {
public static function executeTrade() {
$data = json_decode(file_get_contents('php://input'), true);
// Controller only handles HTTP
$result = TradeService::executeTrade(
$data['user_id'],
$data['symbol'],
$data['type'],
$data['amount']
);
Response::success($result);
}
}
// TradeService.php - Business logic lives here
class TradeService {
public static function executeTrade($userId, $symbol, $type, $amount) {
// Validate balance
$account = TradeAccountService::getAccountById($userId);
if ($account['balance'] < $amount) {
throw new Exception('Insufficient funds');
}
// Get real-time price from Binance
$price = BinanceService::getCurrentPrice($symbol);
// Execute trade
$trade = Trade::create([
'user_id' => $userId,
'symbol' => $symbol,
'type' => $type,
'amount' => $amount,
'price' => $price,
'status' => 'completed'
]);
// Update account balance
TradeAccountService::updateBalance($userId, -$amount);
return $trade;
}
}
The payoff:
- Business logic separated from HTTP concerns
- Reusable services across controllers
- Testable without mocking HTTP requests
- Clear dependency flow
Chapter 3: The WebSocket Revelation—ReactPHP Enters the Chat
Here's where things got interesting. HTTP polling is dead. Long polling is a hack. WebSockets are the future.
The Challenge
Trading platforms need push-based updates:
- Price changes every second
- Order fills happen instantly
- Account balances update in real-time
Traditional PHP dies after each request. How do you keep a persistent connection?
Enter ReactPHP + Ratchet
// websocket/server.php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
require __DIR__ . '/../vendor/autoload.php';
class TradingWebSocket implements MessageComponentInterface {
protected $clients;
protected $binanceClient;
public function __construct() {
$this->clients = new \SplObjectStorage;
$this->connectToBinance();
}
// When a client connects
public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn);
echo "New connection! ({$conn->resourceId})\n";
}
// When Binance sends price update
private function connectToBinance() {
$connector = new \Ratchet\Client\Connector($this->loop);
$connector('wss://stream.binance.com:443/ws/btcusdt@kline_1s')
->then(function($conn) {
$conn->on('message', function($msg) {
$data = json_decode($msg);
$price = $data->k->c; // Close price
// Broadcast to ALL connected clients
$this->broadcastPrice('BTCUSDT', $price);
});
});
}
// Push updates to all clients
private function broadcastPrice($symbol, $price) {
$payload = json_encode([
'type' => 'price_update',
'symbol' => $symbol,
'price' => $price,
'timestamp' => time()
]);
foreach ($this->clients as $client) {
$client->send($payload);
}
}
}
// Start the WebSocket server
$server = IoServer::factory(
new HttpServer(
new WsServer(
new TradingWebSocket()
)
),
8080
);
echo "WebSocket server running on port 8080...\n";
$server->run();
The Magic Explained
- ReactPHP provides an event loop (like Node.js, but in PHP!)
- Ratchet wraps ReactPHP for WebSocket protocol handling
- Server connects to Binance WebSocket stream
- Receives price updates every second
- Broadcasts to all connected frontend clients
No polling. No HTTP overhead. Pure real-time magic. ✨
Chapter 4: The Production Reality—Shared Hosting Strikes Back
The Port 8080 Problem
In development: ws://localhost:8080 worked perfectly.
In production (cPanel): Connection refused 💀
Why? Most shared hosting blocks non-standard ports (8080, 9443) for security.
The Solution: Apache Proxy on Port 443
# websocket/.htaccess
<IfModule mod_proxy.c>
<IfModule mod_proxy_wstunnel.c>
# Proxy WebSocket connections through HTTPS (port 443)
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^(.*)$ ws://127.0.0.1:8080/$1 [P,L]
# Fallback for non-WebSocket requests
RewriteRule ^(.*)$ http://127.0.0.1:8080/$1 [P,L]
</IfModule>
</IfModule>
How it works:
- Frontend connects to
wss://yourdomain.com/app/backend/websocket/(port 443—always open!) - Apache detects
Upgrade: websocketheader - Proxies connection to internal
ws://127.0.0.1:8080 - WebSocket server handles the connection
Result: WebSockets work on ANY hosting with Apache! 🎉
Chapter 5: The Frontend—SPA Without the Framework
The Vanilla JS Module Pattern
// js/modules/websocket.js
export class WebSocketManager {
constructor() {
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
}
connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
const wsUrl = `${protocol}//${host}/app/backend/websocket/`;
this.ws = new WebSocket(wsUrl);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'price_update') {
this.updatePriceUI(data.symbol, data.price);
}
};
this.ws.onclose = () => {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
setTimeout(() => this.connect(), 1000);
this.reconnectAttempts++;
}
};
}
updatePriceUI(symbol, price) {
const priceElement = document.querySelector(`[data-symbol="${symbol}"]`);
if (priceElement) {
priceElement.textContent = `$${parseFloat(price).toFixed(2)}`;
// Add flash animation
priceElement.classList.add('price-flash');
setTimeout(() => priceElement.classList.remove('price-flash'), 300);
}
}
}
// app.js
import { WebSocketManager } from './modules/websocket.js';
import { Router } from './modules/router.js';
import { Auth } from './modules/auth.js';
const app = {
ws: new WebSocketManager(),
router: new Router(),
auth: new Auth(),
init() {
this.router.init();
if (this.auth.isLoggedIn()) {
this.ws.connect();
}
}
};
document.addEventListener('DOMContentLoaded', () => app.init());
Client-Side Routing (No React Router Needed)
// js/modules/router.js
export class Router {
constructor() {
this.routes = {
'/': 'pages/dashboard.html',
'/trade': 'pages/trade.html',
'/history': 'pages/history.html'
};
}
init() {
window.addEventListener('popstate', () => this.loadRoute());
document.addEventListener('click', (e) => {
if (e.target.matches('[data-link]')) {
e.preventDefault();
this.navigate(e.target.href);
}
});
this.loadRoute();
}
async navigate(url) {
history.pushState(null, null, url);
await this.loadRoute();
}
async loadRoute() {
const path = window.location.pathname;
const template = this.routes[path] || this.routes['/'];
const response = await fetch(template);
const html = await response.text();
document.getElementById('app').innerHTML = html;
}
}
Why this approach rocks:
- ✅ No build step (just write code and refresh!)
- ✅ Native ES6 modules (browser-native, no Webpack)
- ✅ Small bundle size (~20KB total JS)
- ✅ Easy debugging (no sourcemaps, no transpilation)
Chapter 6: Progressive Web App—Offline Trading
The Service Worker Strategy
// service-worker.js
const CACHE_VERSION = 'tradity-v1';
const STATIC_CACHE = [
'/',
'/css/app.css',
'/js/app.js',
'/js/modules/websocket.js',
'/img/logo.png'
];
// Install: Cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_VERSION).then((cache) => {
return cache.addAll(STATIC_CACHE);
})
);
});
// Fetch: Network-first for API, cache-first for assets
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Never cache API calls (real-time data!)
if (url.pathname.includes('/api/')) {
event.respondWith(fetch(event.request));
return;
}
// Cache-first for static assets
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
Result:
- App loads instantly (even on slow 3G)
- Works offline for viewing trade history
- Updates in background when online
The manifest.json
{
"name": "Tradity Trading Platform",
"short_name": "Tradity",
"start_url": "/",
"display": "standalone",
"background_color": "#1a1a2e",
"theme_color": "#0f3460",
"icons": [
{
"src": "/img/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/img/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
One tap install on mobile—looks like a native app! 📱
Chapter 7: The Lessons Learned
What Worked Brilliantly
1. Vanilla PHP Scalability
- Deployed on $5/month VPS
- Handles 150+ concurrent WebSocket connections
- Zero framework overhead = fast responses
2. ReactPHP Power
- PHP isn't just for request/response anymore
- Event-driven architecture in PHP (who knew?!)
- Binance WebSocket integration was seamless
3. PWA Capabilities
- Users "install" the app like a native app
- Offline support for critical features
- Push notifications for trade alerts
4. Maintainability
- No framework updates breaking things
- Every line of code is intentional
- Debugging is straightforward (no magic)
The Challenges
1. WebSocket Port Blocking
- Solution: Apache proxy on port 443
- Learning: Production environments are different beasts
2. Browser Caching vs. Service Worker
- Problem: API responses getting cached (disaster for trading!)
- Solution: Explicit cache-control headers + service worker configuration
3. Race Conditions in Production
- Problem: Users seeing wrong account data under load
-
Cause: Type mismatch in SQL queries (
"s"vs"i"binding) - Solution: Strict type declarations in prepared statements
4. MySQL Connections in Long-Running Processes
- Problem: WebSocket server loses DB connection after 8 hours
- Solution: Connection pooling + reconnection logic
Chapter 8: Performance Metrics—The Proof
Backend Response Times
Endpoint Avg Response Time
----------------------------------------
/api/auth/login 45ms
/api/trades/execute 78ms
/api/user/profile 23ms
WebSocket message <5ms (push-based!)
Frontend Load Times
Metric First Load Cached Load
------------------------------------------------
HTML 120ms 5ms
CSS Bundle 45ms 2ms
JS Bundle (20KB) 35ms 1ms
Total Interactive 310ms 15ms
Lighthouse Score: 98/100 🎯
WebSocket Performance
- Message latency: <50ms from Binance → Frontend
-
Concurrent users: 150+ (tested with
ws-benchmark) - Memory usage: 180MB for server process
- Uptime: 99.8% (cron job restarts if crashed)
Chapter 9: Deployment—From Localhost to Production
The Development Environment
XAMPP (Windows)
└── htdocs/
└── tradity-backend/
├── api/
├── services/
├── controllers/
├── websocket/
└── index.php
The Production Setup (cPanel VPS)
# 1. Upload files via Git or FTP
git clone https://github.com/yourusername/tradity-software.git
# 2. Install Composer dependencies
composer install --no-dev --optimize-autoloader
# 3. Setup database
mysql -u root -p < db/migrations/schema.sql
# 4. Start WebSocket server
cd websocket/
php server.php > server.log 2>&1 &
# 5. Setup cron job for auto-restart
*/5 * * * * /bin/bash /path/to/websocket/start_websocket.sh
The Magic Setup Script (No SSH Needed!)
I created a web-based setup wizard for clients:
// websocket/setup.php - Deploy with zero technical knowledge!
<?php
$websocketDir = __DIR__;
$pidFile = "$websocketDir/server.pid";
// One-click server start
if ($_POST['action'] === 'start') {
$command = "nohup /usr/local/bin/php $websocketDir/server.php > $websocketDir/server.log 2>&1 & echo $! > $pidFile";
exec($command);
echo "✅ Server started!";
}
// Display current status
$pid = file_exists($pidFile) ? file_get_contents($pidFile) : null;
$running = $pid && posix_kill($pid, 0);
?>
<div class="status">
<?php if ($running): ?>
✅ Server is running (PID: <?= $pid ?>)
<?php else: ?>
❌ Server is stopped
<?php endif; ?>
</div>
<button onclick="startServer()">▶️ Start Server</button>
<button onclick="stopServer()">⏹️ Stop Server</button>
Clients deploy in 3 clicks. No terminal commands. No SSH panic. 🎉
Chapter 10: The Verdict—Was Vanilla Worth It?
By the Numbers
| Metric | Vanilla Stack | Framework Stack |
|---|---|---|
| Total Code Size | 12MB (with vendor) | 250MB+ (node_modules) |
| Boot Time | 8ms | 50-200ms |
| Memory Footprint | 32MB per process | 100MB+ per process |
| Learning Curve | 2 weeks | 2 months |
| Deployment | Copy files, done | Build, transpile, deploy |
| Long-term Maintenance | Full control | Framework updates required |
The Philosophical Win
I understand every line of code in this project.
No magic. No "it just works" (until it doesn't). When something breaks, I know exactly where to look because I wrote it.
When Vanilla Makes Sense
✅ Use vanilla when:
- You need full control over performance
- You're building long-term (5+ years)
- You want zero dependencies to update
- You're deploying to diverse environments (shared hosting to VPS)
- You want to deeply understand how things work
❌ Use frameworks when:
- Rapid prototyping is priority
- You have a large team with framework experience
- You need battle-tested solutions (authentication, ORM)
- Time-to-market is critical
- You're building standard CRUD apps
Epilogue: What's Next?
This project taught me that modern web development doesn't require modern frameworks. PHP 8, vanilla JavaScript, and WebSockets are powerful enough to build production-grade real-time applications.
Future Enhancements
- Multi-exchange support (Coinbase, Kraken via adapter pattern)
- Technical indicators (Moving averages, RSI, Bollinger Bands)
- Social trading (Copy trades from top performers)
- Mobile apps (PWA already works, native iOS/Android next)
Open Source?
I'm considering open-sourcing the core platform. If you're interested, drop a comment!
The Code
You can explore the source code at:
- Backend: github.com/joshike-code/tradity-software
- Live Demo and full source code: degiantstore.live
Final Thoughts
Building Tradity was a rebellion against the framework-first mindset. It proved that fundamentals matter more than trends.
Sure, Laravel would've given me authentication scaffolding. React would've given me state management. But at what cost?
I traded convenience for control, and I'd do it again.
Because at the end of the day, when your users are trading real money (even simulated), you need to understand every single line of code that stands between their click and the database.
Vanilla PHP and vanilla JavaScript gave me that understanding.
And that's priceless.
What's your take? Are frameworks overhyped, or am I a masochist? Let's debate in the comments! 👇
Bonus: Resources for Aspiring Vanilla Builders
PHP WebSockets:
Vanilla JS Patterns:
PWA Guide:
Trading APIs:
Happy coding, and may your trades always be profitable! 📈
Top comments (0)