Willow SoftwareEngineeringBuild notes

Why Salix Monitor 360 was built on LAMP without a PHP framework

The engineering reasons behind building Salix Monitor 360 on a LAMP stack without a heavyweight PHP framework.

Published 8 min readGraeme Moignard
Scroll to read

Engineering • Build notes

Why Salix Monitor 360 was built on LAMP without a PHP framework

Written by Graeme Moignard

Published:
Last updated:

At a glance

A quick orientation before the deeper read.

Focus Why the app uses a direct LAMP approach
Useful for Clients and technical readers curious about the engineering choice
Takeaway Simple architecture can be a strength when it is maintained deliberately

Salix Monitor 360 is a big build. Monitoring, alerting, dashboards, reporting, payments, tenancy, permissions, integrations, plus the boring-but-critical bits like logging, backups, hygiene and security posture. When you are building a platform like that on your own, your biggest enemy isn’t PHP… it’s time.

Salix Monitor 360 uses a straight LAMP approach (Linux, Apache, MySQL, PHP) without a heavyweight PHP framework. Not because frameworks are “bad”, but because for this product, at this stage, the fastest path to a stable, secure, shippable system is a small, deliberate codebase with end-to-end control.

At a glanceA straightforward development cycleA simple view of the workflow discussed in this article.
01Plan
02Build
03Test
04Publish

The real trade-off: shipping product vs shipping architecture

This is a commercial platform, not a demo. The priority is getting real features working reliably: monitors, checks, alert routing, incident workflow, role-based access, organisation boundaries, account management, PDF exports, reporting, scheduled jobs, audits and all the operational glue you only discover once it’s running.

A framework gives you a lot out of the box. It also gives you a lot to carry: upgrades, breaking changes, dependency churn, opinionated structure, extra layers and the temptation to “do it the framework way” even when the framework way is slower for the problem you are actually solving.

Why LAMP makes sense for Salix Monitor 360

  • Fewer moving parts. Less surface area, fewer surprises, easier fault-finding.
  • Performance you can reason about. No hidden ORM behaviour, no accidental N+1 queries, no surprise middleware stacks.
  • Security is clearer. Entry points can be audited directly instead of inheriting a huge default feature set.
  • Upgrades are simpler. PHP version upgrades are enough without being pinned to a framework release train.
  • Operational fit. Apache, cron and PHP-FPM patterns are boring (good) and widely understood.

The trick is not “raw PHP everywhere”. It’s structured PHP: consistent patterns, reusable helpers, a small routing layer and disciplined boundaries.

Frameworks aren’t the enemy, overhead is

This is not an anti-framework position. With a team, a tight deadline and a mostly standard CRUD application, a framework can be an obvious win. But Salix Monitor 360 isn’t just CRUD.

It’s a monitoring and operations platform. A lot of work lives in:

  • network checks and timing logic
  • alert routing and escalation
  • event ingestion and reduction
  • multi-tenant scoping
  • report generation and exports
  • customer account flows and provider quirks

Framework conventions don’t solve those. They mostly affect how you wire pages together. Only the wiring actually needed was built, and it was kept lean.

“No framework” doesn’t mean “no structure”

The biggest criticism of rolling your own is that it can become a messy pile. That’s fair if structure is not imposed. The approach is to keep the architecture small, explicit and repeatable.

A simple pattern that scales

  • Front controller (one entry point)
  • Router (maps URL → handler)
  • Guards (login + permissions)
  • Services (storage, account flows, exports, etc.)
  • Templates (includes/layout partials)

Code example: one entry point (front controller)

Frameworks usually route everything through a single entry point. You can do the same in LAMP with one rewrite rule.

# .htaccess (concept)
RewriteEngine On

# Allow real files/directories
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]

# Everything else goes to index.php
RewriteRule ^ index.php [L]

That gives you a clean, predictable “every request comes through one place” model, which is exactly what you want for authentication, tenancy checks and consistent error handling.

Code example: a tiny router (explicit beats magic)

A platform routing layer can stay simple: plain mapping, readable handlers, no surprises.

<?php
// routes.php (concept)
return [
  'GET /admin'              => ['AdminController', 'dashboard'],
  'GET /dashboard'          => ['DashboardController', 'index'],
  'POST /items'             => ['ItemsController', 'create'],
  'GET /reports'            => ['ReportsController', 'index'],
  'POST /events'            => ['EventsController', 'store'],
];
<?php
// index.php (concept)
$routes = require __DIR__ . '/routes.php';

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$path   = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$key    = $method . ' ' . rtrim($path, '/');
if ($key === 'GET') $key = 'GET /';

if (!isset($routes[$key])) {
  http_response_code(404);
  require __DIR__ . '/views/errors/404.php';
  exit;
}

[$class, $action] = $routes[$key];

require __DIR__ . '/bootstrap.php';

$controller = new $class($container);
$controller->$action();

You can make this more advanced (route params, groups, middleware), but the point is that you only add complexity when the product demands it , not because a framework shipped it.

Code example: database access without ORM pain

ORMs are handy until they are not. For monitoring and account-heavy systems, important data paths should stay explicit and understandable. PDO with prepared statements is boring, fast and safe.

<?php
// db.php (concept)
function db(): PDO {
  static $pdo;
  if ($pdo instanceof PDO) return $pdo;

  $pdo = new PDO(
    'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4',
    DB_USER,
    DB_PASS,
    [
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
      PDO::ATTR_EMULATE_PREPARES => false,
    ]
  );

  return $pdo;
}
<?php
// Example query (concept)
$stmt = db()->prepare('
  SELECT id, name, status
    FROM monitors
   WHERE organisation_id = :org
   ORDER BY created_at DESC
   LIMIT 200
');
$stmt->execute([':org' => $orgId]);
$rows = $stmt->fetchAll();

No magic, no hidden queries, no guessing what the ORM is doing. When performance matters (and it does in monitoring), this clarity pays back constantly.

Security example: tenancy and permission guards

Multi-tenant code lives or dies by discipline. A framework doesn’t automatically save you from accidental data leaks. The best defence is consistent guard code that you call everywhere.

<?php
// guard.php (concept)
function require_login(): void {
  if (empty($_SESSION['user_id'])) {
    header('Location: /login');
    exit;
  }
}

function require_org_scope(int $orgId): void {
  // Enforce tenant boundary
  if ((int)($_SESSION['org_id'] ?? 0) !== (int)$orgId) {
    http_response_code(403);
    exit('Forbidden');
  }
}

function require_permission(string $perm): void {
  $perms = $_SESSION['perms'] ?? [];
  if (!in_array($perm, $perms, true)) {
    http_response_code(403);
    exit('Forbidden');
  }
}

The point is to keep the “rules of the world” obvious, testable and hard to bypass by accident. Simple functions, called everywhere, win.

Templating example: includes are fine when you keep them disciplined

You don’t need a templating engine to get reuse and consistency. For a SaaS admin UI, includes and partials are often enough: one layout, shared navigation, consistent escaping and small view files.

<?php
// render.php (concept)
function e(string $s): string {
  return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function render(string $view, array $data = []): void {
  extract($data, EXTR_SKIP);
  require __DIR__ . '/views/partials/header.php';
  require __DIR__ . '/views/' . $view . '.php';
  require __DIR__ . '/views/partials/footer.php';
}

That’s enough structure to avoid copy/paste soup, while keeping the “how it works” completely transparent.

Jobs, schedules and monitoring logic

Monitoring platforms always need background jobs: checks, retries, notifications, aggregation, cleanup, exports. In LAMP land, cron is still a solid tool: predictable, easy to check and easy to run on any Linux box.

# Cron (concept)
* * * * * /usr/bin/php /var/www/sm360/bin/run_checks.php >> /var/log/sm360/checks.log 2>&1
*/5 * * * * /usr/bin/php /var/www/sm360/bin/process_alerts.php >> /var/log/sm360/alerts.log 2>&1
0 2 * * * /usr/bin/php /var/www/sm360/bin/cleanup.php >> /var/log/sm360/cleanup.log 2>&1

A framework job queue can be brilliant. But you also inherit queue infrastructure, supervisors and deployment complexity. For early and mid-stage product build, cron plus clean scripts is often the fastest route to “it runs every time”.

“But frameworks give you best practices”

They can. They also give you defaults that may not match your threat model, your tenancy model, or your performance goals. Best practice isn’t something you install; it’s what you enforce.

The best practices used in Salix Monitor 360 are simple and relentless:

  • Prepared statements everywhere (no string-built SQL).
  • Consistent permission checks at every boundary.
  • Consistent output escaping in templates.
  • Clear logging so production issues aren’t guesswork.
  • Small, auditable dependencies and controllable upgrades.

Where a framework would be a good fit, and why this build still avoids one

If Salix Monitor 360 was a content site, a simple CRM, or a basic admin panel with a couple of models, a framework would likely be the sensible choice. The issue is not capability; it is focus.

Salix Monitor 360 is a platform where the complexity belongs in the operational domain, not the routing layer. The routing layer therefore stays thin and predictable, with development time focused on what customers actually pay for: monitoring that’s reliable, alerts that are actionable and a system that behaves.

Final thoughts

The “right” stack is the one that helps you ship a reliable product without painting yourself into a corner. For Salix Monitor 360, LAMP without a heavyweight PHP framework is the right move: it’s fast to build, easy to operate, and it keeps the system understandable.

As the platform grows, additional structure can be added where it genuinely earns its keep, without starting with a tower of abstractions just to feel enterprise.

If you are building something large and real, don’t let anyone shame you for choosing boring technology. Boring tech is often the reason the product actually ships.

More from Willow Software

Browse the full blog index for Salix Monitor 360 updates, security notes and web development articles.

Open blog index