# Architecture — A&B Hub Backend

## 1. What this is, and what it isn't

This is a **plain PHP 8.3 MVC application** — no Laravel, no Symfony,
no framework dependency for the core request lifecycle. That's a
deliberate choice, not a limitation: a CRM this data-heavy, run by a
small team, benefits far more from a codebase where "what happens when
a request comes in" can be read start-to-finish in a handful of small
files than from a full framework's convention-over-configuration
magic. Composer is still used — for PHPMailer, and optionally for
`vendor/autoload.php` once installed — but the app boots and runs
correctly even before `composer install` has ever been executed (see
§4, Autoloading).

Every claim below was verified by actually running the code against a
live MySQL 8 instance during development, not just linted — see the
inline comments in `bin/migrate.php`/`bin/seed.php`-adjacent code for
what was proven and how.

## 2. Folder structure

```
/app
  /controllers     Feature controllers (empty in Phase 1 — see README.md inside)
  /core             Framework internals: Router, Request, Response, Database,
                    Session, Auth, Rbac, Validator, BaseController, BaseModel...
  /helpers          Stateless utility classes: AjaxResponse, Paginator, UploadHelper,
                    plus app/helpers/functions.php (global helper functions)
  /middleware       AuthMiddleware, PermissionMiddleware, CsrfMiddleware, RateLimitMiddleware...
  /models           Feature models (empty in Phase 1 — see README.md inside)
  /services         Business-logic-adjacent, cross-cutting services: AuthService,
                    ActivityLogger, NotificationService, MailService, RateLimiter
  /views            Server-rendered PHP views + the shared layout
/config             One file per concern: app, database, session, mail, security, permissions
/routes             web.php (pages) and api.php (JSON endpoints), registered on one Router
/database
  /migrations       Numbered, ordered .sql files — the deployable unit
  /seeders          PHP classes that populate reference/demo data
  schema.sql         The single source of truth migrations are generated from
/storage
  /logs /cache /sessions /framework   Never web-accessible (see §6)
/uploads            User-uploaded files — never web-accessible, never PHP-executable
/public             The ONLY web-server document root
  index.php          The one PHP file the web server ever executes directly
  .htaccess
  /assets            The completed frontend's css/js (copied in verbatim)
/bin                CLI tools: migrate.php, seed.php, generate_key.php
/docs               This file and its companions
/vendor             Composer packages (git-ignored, generated by `composer install`)
```

### Why `App\Core\BaseModel` / `App\Core\BaseController`, not `App\Models\BaseModel`?

Every *concrete* model (`LeadModel`, `CustomerModel`, ...) lives in
`app/models` as `App\Models\*`. The abstract base classes they extend
live in `app/core` instead, under `App\Core\BaseModel` and
`App\Core\BaseController`. This mirrors how most mature PHP frameworks
draw the line: framework machinery lives in "core," and only classes a
feature developer actually writes live in the feature-facing folder.
Opening `app/models/` should show a new developer nothing but their
own domain code — not the plumbing underneath it.

## 3. Request lifecycle

1. **Every** HTTP request hits `public/index.php` — Apache's rewrite
   rule in `public/.htaccess` sends anything that isn't a real static
   file there.
2. `index.php` loads the autoloader (Composer's if present, our
   fallback `App\Core\Autoloader` if not), loads
   `app/helpers/functions.php`, then calls `Application::boot()`.
3. `Application::boot()` loads `.env`, sets the timezone, configures
   the logger, and registers `ErrorHandler` — from this point on, no
   PHP error or uncaught exception can crash the app with a raw white
   screen; it's always caught, logged to `storage/logs`, and turned
   into an appropriate response (detailed in debug mode, generic in
   production).
4. `Application::handle()` starts the session (`Session::start()`,
   which applies every cookie-security setting from
   `config/session.php` *before* `session_start()` is ever called),
   captures the request into an `App\Core\Request` object, and hands
   it to the `Router`.
5. `Router::dispatch()` matches the path + HTTP method against routes
   registered in `routes/web.php`/`routes/api.php`, builds the
   middleware pipeline for that specific route (`auth`, `csrf`,
   `permission:*`, `throttle:*`, in the order listed), and finally
   invokes the controller action (or closure).
6. The controller returns an `App\Core\Response` object — nothing is
   ever echoed directly from a controller. `Response::send()` is the
   single place HTTP status codes, security headers, and body content
   are actually written to the client.
7. Any `ValidationException` thrown during step 5/6 is caught centrally
   by `Application::handle()` and turned into a 422 JSON response (AJAX)
   or a flashed-errors redirect (server-rendered form) — a controller
   never has to remember to catch this itself.

## 4. Autoloading

`composer.json` declares `"App\\": "app/"` under PSR-4 — once
`composer install` has run, Composer's generated `vendor/autoload.php`
is what actually resolves every class, and it's what `public/index.php`
prefers if present. Before that first `composer install` (a fresh
checkout, or a CI step that hasn't run yet), `App\Core\Autoloader`
registers itself as a fallback so the app still boots. It resolves
`App\*` (mapped to `/app`) and `Database\Seeders\*` (mapped to
`/database/seeders`) — the two namespaces the app defines outside of
what Composer's own autoloader would give it.

## 5. Routing & middleware

`App\Core\Router` is intentionally minimal: static segments,
`{parameters}`, and an explicit middleware list per route — no
attribute scanning, no route caching step to remember to run. A route
is registered like:

```php
$router->post('/leads', 'App\Controllers\LeadController@store',
    middleware: ['auth', 'csrf', 'permission:leads_customers.create']);
```

Middleware run in the order listed, each wrapping the next like layers
of an onion (`AuthMiddleware` first means an anonymous visitor is
redirected to `/login` before `PermissionMiddleware` ever gets a
chance to return a raw 403). Parameterized middleware
(`permission:leads_customers.create`, `throttle:strict`) split on the
first `:` — everything after it is passed to that middleware's
constructor.

## 6. Why `/public` is the document root (and nothing else is)

The web server's `DocumentRoot` **must** point at `/public`. Everything
that matters — `.env`, `config/`, `app/`, `database/` (including
`schema.sql` and every migration), `storage/logs` — lives *outside*
that directory entirely, so there is no `.htaccess` rule standing
between an attacker and those files; they are simply not reachable by
any URL, full stop. The root `.htaccess` (`Require all denied`) exists
purely as a defense-in-depth backstop for the case where a server is
ever misconfigured to serve from the project root instead.

`uploads/` gets its own `.htaccess` disabling script execution
(`php_flag engine off` + denying script-like extensions outright) as a
second independent layer on top of `UploadHelper` already renaming
every file to a random, extension-sniffed-from-real-content name on
disk — belt and suspenders, because file upload handling is exactly
the kind of thing worth over-engineering.

## 7. Database access

There is exactly one way to talk to the database: `App\Core\Database::
connection()`, a PDO singleton configured with `ATTR_EMULATE_PREPARES
=> false` (so MySQL performs *real* server-side prepared statements,
not PHP-emulated ones) and `ATTR_ERRMODE => ERRMODE_EXCEPTION`. No
model, service, or controller anywhere in this codebase builds SQL by
string concatenation — every value is bound.

`App\Core\BaseModel` gives every future model `find`/`all`/`paginate`/
`insert`/`update`/`delete`/`restore`/`count` for free, with automatic
`created_at`/`updated_at` and `created_by`/`updated_by` stamping and
automatic soft-delete handling (`deleted_at IS NULL` filtering,
`UPDATE ... SET deleted_at = NOW()` instead of a real `DELETE`) for any
table that has those columns. It is deliberately *not* a full ORM —
no lazy relationship loading, no query builder — because explicit SQL
in each model's own domain-specific methods (`LeadModel::
findByStage()`, etc.) stays far more debuggable than an abstraction
that hides what's actually being queried, especially once reporting
queries start joining six tables together.

## 8. Authentication & RBAC — how they fit together

- `App\Core\Auth` answers "who is logged in right now?" — it reads/
  writes the session-stored user id and caches the full user row for
  the duration of one request. It does **not** know how to verify a
  password.
- `App\Services\AuthService` owns the actual login flow: checking the
  login rate limit (`login_attempts` table), verifying the password
  hash, recording the attempt either way, and — only on success —
  calling `Auth::login()`. This split matters: `Auth` is safe to call
  from anywhere (middleware, views, models) without pulling in rate
  limiting or password-hashing concerns; `AuthService` is the one
  place those concerns live.
- `App\Core\Rbac` resolves *what* a logged-in user can do: role
  permissions from `role_permissions`, with per-user
  `user_permission_overrides` (allow or deny) layered on top. A denial
  always wins over an allow, and a specific user override always wins
  over their role's default.
- `App\Middleware\PermissionMiddleware` is the enforcement point —
  attach `permission:{module}.{action}` to any route and an
  unauthorized user gets a 403 (or a 401-shaped JSON body for AJAX)
  before the controller ever runs.

`config/permissions.php` is the single source of truth for which
modules and actions exist, and what each of the four seeded system
roles (Operations Admin, Sales Rep, Field Staff, Read-Only) gets by
default — it's read by both `RolePermissionSeeder` (to populate the
database) and, going forward, by anything that needs to render the
Roles & Permissions matrix UI without duplicating that list.

## 9. How a new module plugs in (worked example: Leads)

This is the walkthrough Phase 2 should follow for every module:

1. **Model** — `app/models/LeadModel.php`, extends `App\Core\BaseModel`,
   declares `$table = 'leads'`, `$fillable`, and `$softDeletes = true`.
   See `app/models/README.md` for the full example.
2. **Controller** — `app/controllers/LeadController.php`, extends
   `App\Core\BaseController`. Every write action starts with
   `$this->validateOrFail($request, [...])`; every read action that
   returns JSON uses `$this->json(...)` or `App\Helpers\AjaxResponse`.
   See `app/controllers/README.md`.
3. **Validation rules** — defined inline in the controller action (or
   extracted to a small rule-array method if reused across
   create/update) using `App\Core\Validator`'s `"field" =>
   "required|email|max:191|unique:leads,email"` syntax.
4. **Routes** — added to `routes/web.php` (page routes) and/or
   `routes/api.php` (JSON endpoints), each with `['auth',
   'permission:leads_customers.{action}']` — the exact permission slugs
   already exist from Phase 1's seeded RBAC data, no new migration
   needed.
5. **Activity/audit trail** — call `App\Services\ActivityLogger::
   record()` after any meaningful write; it fills in both the
   per-record timeline (`activities` table, powers the Lead Details
   "Activity Timeline") and the system-wide audit trail
   (`activity_logs`, powers Activity Logs) in one call.
6. **Notifications** — call `App\Services\NotificationService::notify()`
   or `::notifyBusinessLine()` wherever the frontend implies a
   notification should fire (e.g. a new lead created).
7. **View** — a PHP file in `app/views/leads/*.php`, rendered via
   `$this->view('leads/index', [...])`, whose markup is ported over
   from the already-completed static `leads.html` template (same CSS
   classes, same Bootstrap components — only the data becomes dynamic).

No step in that list requires touching `app/core`, adding a new config
file, or changing the routing/middleware system — that's the point of
having built it first.

## 10. Security posture summary

See `docs/CODING_STANDARDS.md` §Security for the enforced rules. In
one paragraph: every query is parameterized (PDO prepared statements,
emulation disabled); every password is hashed with Argon2id (bcrypt
fallback) and transparently re-hashed on login if the policy changes;
every state-changing request requires a valid CSRF token; every output
of user-supplied or DB-sourced text goes through `e()`; every session
cookie is `HttpOnly`, `SameSite=Lax`, and `Secure` outside local dev;
every file upload is renamed to a random name, MIME-sniffed against an
allow-list (never trusting the client's claimed type), and stored
somewhere the web server refuses to execute PHP from; and every login
attempt — successful or not — is rate-limited and logged.
