In 2002, a question that every developer building a web application had to answer was: what do I run this on? The enterprise answer was Windows 2000 Server + IIS + SQL Server + ASP - expensive, license-heavy, requiring Windows expertise. The open-source answer was LAMP: Linux, Apache, MySQL, PHP.
LAMP was not a product. Nobody invented it. It emerged organically as the cheapest, most deployable combination of open-source tools that worked together reliably. By 2002 it was not just viable - it was dominant for new web projects. Shared hosting plans worldwide ran Apache on Linux. MySQL was included free. PHP came bundled. You could deploy a working web application for $8 a month.
This changed everything about who could build on the web.
The Components in 2002
Linux (Red Hat 7.3 / Debian 3.0): The OS. Free. Stable. Ran Apache without a Windows license. Kernel 2.4 was the production standard.
Apache 1.3 / 2.0: The web server. mod_php loaded PHP as an Apache module - no CGI spawning, no process per request. PHP ran in-process, dramatically faster than Perl CGI.
MySQL 3.23 / 4.0: The database. No transactions in 3.23 (MyISAM only). MySQL 4.0 (2002) added InnoDB with transactions as a stable option. Fast for reads. Good enough for most web applications.
PHP 4.3: The language. register_globals was on by default (a security disaster that took years to kill). Procedural code embedded in HTML. Fast to write, fast to deploy.
A Real 2002 PHP + MySQL Application
<?php
// config.php - database connection, 2002 style
// No PDO (not until PHP 5.1). No MySQLi (PHP 4 only had mysql_* functions).
define('DB_HOST', 'localhost');
define('DB_USER', 'webapp');
define('DB_PASS', 'secret123');
define('DB_NAME', 'myapp');
$conn = mysql_connect(DB_HOST, DB_USER, DB_PASS);
if (!$conn) {
die('Database connection failed: ' . mysql_error());
}
mysql_select_db(DB_NAME, $conn);
mysql_query("SET NAMES 'utf8'", $conn);
?>
<?php
// products.php - product listing page
require_once 'config.php';
require_once 'session.php';
$category_id = isset($_GET['cat']) ? (int)$_GET['cat'] : 0;
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$per_page = 20;
$offset = ($page - 1) * $per_page;
// In 2002, most developers concatenated SQL directly.
// The correct approach: cast to int (we did, above) to prevent injection.
if ($category_id > 0) {
$where = "WHERE category_id = $category_id AND active = 1";
} else {
$where = "WHERE active = 1";
}
$total_result = mysql_query(
"SELECT COUNT(*) AS total FROM products $where",
$conn
);
$total_row = mysql_fetch_assoc($total_result);
$total_pages = ceil($total_row['total'] / $per_page);
$result = mysql_query(
"SELECT id, name, price, image, description
FROM products
$where
ORDER BY created_at DESC
LIMIT $offset, $per_page",
$conn
);
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Products - My Store</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="/css/style.css" />
</head>
<body>
<?php include 'header.php'; ?>
<div id="content">
<h1>Products</h1>
<?php if (mysql_num_rows($result) === 0): ?>
<p>No products found.</p>
<?php else: ?>
<table cellpadding="8" cellspacing="0" border="0" class="product-table">
<tr>
<th>Image</th>
<th>Product</th>
<th>Price</th>
<th>Action</th>
</tr>
<?php while ($row = mysql_fetch_assoc($result)): ?>
<tr class="<?php echo ($i++ % 2 === 0) ? 'even' : 'odd'; ?>">
<td>
<img src="/images/products/<?php echo htmlspecialchars($row['image']); ?>"
width="60" height="60" alt="<?php echo htmlspecialchars($row['name']); ?>" />
</td>
<td>
<a href="product.php?id=<?php echo $row['id']; ?>">
<?php echo htmlspecialchars($row['name']); ?>
</a>
<br />
<small><?php echo htmlspecialchars(substr($row['description'], 0, 80)); ?>...</small>
</td>
<td>$<?php echo number_format($row['price'], 2); ?></td>
<td>
<a href="cart.php?action=add&id=<?php echo $row['id']; ?>">Add to Cart</a>
</td>
</tr>
<?php endwhile; ?>
</table>
<!-- Pagination - hand-built, no framework -->
<div class="pagination">
<?php for ($p = 1; $p <= $total_pages; $p++): ?>
<?php if ($p == $page): ?>
<strong><?php echo $p; ?></strong>
<?php else: ?>
<a href="?cat=<?php echo $category_id; ?>&page=<?php echo $p; ?>"><?php echo $p; ?></a>
<?php endif; ?>
<?php endfor; ?>
</div>
<?php endif; ?>
</div>
<?php include 'footer.php'; ?>
</body>
</html>
The MySQL Schema (2002)
-- MySQL 4.0, MyISAM engine (InnoDB was optional, not default)
-- Full-text search built into MyISAM - one real advantage over InnoDB at the time
CREATE TABLE categories (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY (slug)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE products (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
category_id INT UNSIGNED NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
image VARCHAR(255),
active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (id),
KEY idx_category (category_id),
KEY idx_active (active),
FULLTEXT idx_search (name, description)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-- Session table - storing PHP sessions in MySQL (not files)
-- For multi-server setups or when /tmp wasn't shared
CREATE TABLE sessions (
session_id VARCHAR(32) NOT NULL,
data TEXT,
last_access INT UNSIGNED NOT NULL,
PRIMARY KEY (session_id)
) ENGINE=MyISAM;
Apache Configuration: .htaccess
Every LAMP developer in 2002 knew .htaccess by heart:
# .htaccess - the 2002 developer's Swiss Army knife
# URL rewriting: /product/42 → /product.php?id=42
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^product/([0-9]+)/?$ product.php?id=$1 [L,QSA]
RewriteRule ^category/([a-z0-9-]+)/?$ products.php?slug=$1 [L,QSA]
# PHP configuration overrides (shared hosting didn't give php.ini access)
php_flag register_globals Off # critical security fix
php_flag display_errors Off # never on production
php_value upload_max_filesize 5M
php_value post_max_size 6M
# Prevent directory listing
Options -Indexes
# Protect config files
<FilesMatch "^(config|db)\.php$">
Order allow,deny
Deny from all
</FilesMatch>
# Cache static assets
<FilesMatch "\.(jpg|jpeg|gif|png|css|js)$">
ExpiresActive On
ExpiresDefault "access plus 7 days"
</FilesMatch>
What LAMP Got Right
Zero licensing cost. A complete production web stack cost nothing in software licenses. The only cost was hardware or hosting. This made the web accessible to individuals, startups, and developing-world developers in a way that Windows/IIS could not.
Shared hosting compatibility. Every shared hosting provider in 2002 ran Apache on Linux with PHP and MySQL. You could develop on a $8/month plan, test there, and move to a dedicated server when you outgrew it. The stack was identical everywhere.
mod_php performance. PHP running inside Apache as a module was dramatically faster than CGI. No process spawning per request. The interpreter stayed resident in memory. This was good enough for millions of page views per day on modest hardware.
What LAMP Got Wrong
register_globals. PHP 4's register_globals = On by default meant GET/POST parameters were automatically available as global variables. $id was set if ?id=5 was in the URL. This made injection attacks trivial. Turning it off was the first security fix any competent developer applied - but shared hosting often couldn't be configured, and developers who didn't know any better left it on.
No transactions by default. MyISAM had no transactions. A partial insert during a crash left the database inconsistent. Most 2002 LAMP applications were not using InnoDB and were not thinking about this. Order tables with no transaction guarantees were a real problem.
No prepared statements. The mysql_* extension had no prepared statement support. SQL injection protection required careful manual escaping with mysql_real_escape_string() - which many developers forgot, misapplied, or had never heard of.
The LAMP stack won the web despite these flaws, not because it was architecturally superior. It won because it was free, everywhere, and good enough. The problems it had were solved over the next decade - PDO in PHP 5.1, InnoDB as default in MySQL 5.5, register_globals deprecated in PHP 5.3 and removed in PHP 5.4. The platform matured around its weaknesses.
By 2005 it was the most deployed web stack on earth. By 2010 it was the foundation of WordPress, Drupal, Joomla, MediaWiki - the software running most of the web's content.
Aunimeda builds production-grade backend systems - APIs, microservices, real-time applications, and system integrations.
Contact us for backend engineering services. See also: Custom Software Development, Web Development