# INVEETAIRE
## DEVELOPMENT BLUEPRINT

Version 1.1

Status: Active

Project: Inveetaire – Wedding Management Platform

Development Standards · Project Structure · Coding Philosophy · Module Conventions

| Field | Value |
|-------|-------|
| Document Type | Development Blueprint (DBP) |
| Version | 1.1 |
| Status | Active — Synchronized with Sprint 0 |
| Based On | PRD v1.1 · SAD v1.1 · DDD v1.0 · DDD v1.1 (All Approved) |
| Backend Stack | Native PHP 7.4 · MariaDB · Apache |
| Frontend Stack | HTML5 · TailwindCSS · Alpine.js · FontAwesome |
| Hosting | Shared Hosting — cPanel / Rumahweb |
| Change History | v1.0 — Initial Blueprint |
| | v1.1 — Synchronized with DESIGN_REVISION_PACK_v2.0 (REV-010, REV-011, REV-013, REV-015–020, REV-022, REV-023) |

> **Scope of this revision**: All chapters reviewed. Chapters 4, 5, 8, 9, 10, 12, 13 updated. All other chapters unchanged. No architecture was redesigned — only Sprint 0 implementation findings were synchronized.

---

**PURPOSE**

This document defines HOW Inveetaire is built. It is not architecture. It is not database design. It is the engineering contract — the conventions, standards, and rules that every developer and AI assistant must follow when writing code for Inveetaire.

---

## 1. Development Philosophy

Every line of code written for Inveetaire must be answerable to one question: does this make the system simpler, clearer, and easier to maintain — or does it make it more complex? If the answer is the latter, the approach is wrong regardless of how technically impressive it is.

### 1.1 Convention Over Configuration

Inveetaire follows consistent naming, file placement, and structural patterns throughout the codebase. When a developer knows the convention, they know where to find any file, what any class is responsible for, and how any module is structured — without needing documentation for every individual decision.

- A module named Guest always has the same internal structure as a module named Vendor.
- A Controller file for Guest is always named GuestController. A Service is always GuestService. A Model is always GuestModel.
- Configuration keys follow the same naming pattern across all modules.
- Route patterns for every module follow the same namespace and verb conventions.

> **RULE**: If a developer has to guess where something is, the convention has failed. The location of every file must be predictable from the module name alone.

### 1.2 Keep It Simple

No abstraction is added unless it removes real, present duplication. No design pattern is applied unless it solves a problem that exists in the current code. No infrastructure component is introduced unless the existing stack cannot handle the requirement without it.

- A function that is called once does not need to be abstracted.
- A query that runs once per request does not need a caching layer.
- A module that has three responsibilities needs to be split — not given a more complex design.
- If the solution requires explaining the explanation, it is too complex.

### 1.3 Modular First

Every feature belongs to exactly one module. Modules own their controllers, services, models, views, and routes. No code reaches into another module's internals. When Module A needs data from Module B, it calls Module B's public service interface — nothing else.

### 1.4 No Business Logic Inside Views

Views are responsible for displaying data. They receive fully prepared data from controllers. They do not query databases. They do not call service functions. They do not make decisions about what to display based on raw database values.

- A view receives a variable called `$rsvp_status_label` — it does not receive `$rsvp_status` and decide what label to display.
- A view receives a `$can_check_in` boolean — it does not evaluate session roles to determine this.
- Any conditional display logic that requires more than a simple if/else on a prepared variable belongs in the controller or service.

### 1.5 Thin Controllers

Controllers are traffic directors. They receive a request, validate the input, call the appropriate service, and return a response. They contain no business logic. They contain no database queries. They contain no formatting or transformation logic.

- A controller method should rarely exceed 20 lines.
- If a controller method is doing more than receive, validate, delegate, respond — it is doing too much.
- Business rules live in services. Data access lives in models. Display lives in views. Controllers connect them.

### 1.6 Reusable Components

UI elements that appear in more than one place are defined once as a component and included everywhere. Components live in `app/views/components/`. A component receives its data as variables — it never fetches its own data. A component with business logic is a design mistake.

### 1.7 Future Migration Friendly

Every development decision is made with the knowledge that this codebase will eventually migrate to Laravel.

- Module service classes map directly to Laravel service classes.
- Route definitions follow conventions that map to Laravel route groups.
- Database queries are written through model methods — never as raw SQL strings in controllers.
- Configuration is read from configuration files — never hardcoded in controllers or views.

---

## 2. Project Folder Structure

The folder structure is the physical expression of the module architecture. Every directory has a single responsibility. No file lives in a directory whose name does not describe its purpose.

```
inveetaire/
├── app/
│   ├── modules/              # One subdirectory per product module
│   │   ├── Auth/
│   │   ├── Workspace/
│   │   ├── Invitation/
│   │   ├── Guest/
│   │   ├── RSVP/
│   │   ├── Checkin/
│   │   ├── Dashboard/
│   │   ├── Vendor/
│   │   ├── Communication/
│   │   ├── Theme/
│   │   ├── Media/
│   │   ├── Audit/
│   │   ├── Notification/
│   │   ├── Printer/
│   │   ├── Report/
│   │   ├── Settings/
│   │   └── Support/
│   ├── core/                 # Framework kernel classes
│   │   ├── Application.php
│   │   ├── Autoloader.php
│   │   ├── BaseController.php
│   │   ├── BaseModel.php
│   │   ├── BaseService.php
│   │   ├── Bootstrap.php
│   │   ├── Csrf.php
│   │   ├── Database.php
│   │   ├── ErrorHandler.php
│   │   ├── FlashMessage.php
│   │   ├── Logger.php
│   │   ├── MiddlewarePipeline.php
│   │   ├── Request.php
│   │   ├── Router.php
│   │   └── Session.php
│   ├── middleware/           # Concrete middleware classes (built per-sprint)
│   ├── helpers/              # Global helper functions
│   │   ├── config.php        # config($key, $default)
│   │   ├── csrf_field.php    # csrf_field()
│   │   ├── env.php           # env($key, $default)
│   │   └── escape_html.php   # escape_html($value)
│   ├── services/             # Shared services (not module-specific)
│   └── views/
│       ├── layouts/          # Portal layout shells
│       ├── components/       # Reusable UI components
│       └── errors/           # HTTP error pages
├── config/                   # PHP config arrays
│   ├── app.php
│   ├── database.php
│   ├── modules.php
│   ├── session.php
│   └── storage.php
├── routes/                   # Route registration files
│   ├── admin.php
│   ├── couple.php
│   ├── crew.php
│   ├── public.php
│   └── support.php
├── public/                   # Web root — only publicly accessible directory
│   ├── index.php             # Single entry point
│   ├── .htaccess             # mod_rewrite rules
│   └── assets/
│       ├── css/
│       ├── js/
│       └── fonts/
├── storage/
│   ├── cache/
│   ├── exports/
│   ├── logs/
│   ├── reports/
│   ├── sessions/
│   └── uploads/
├── themes/
├── .env                      # NOT committed
├── .env.example              # Committed with all keys, no values
├── .gitignore
├── .htaccess                 # Root-level redirect to public/
└── README.md
```

> **RULE**: No PHP file other than `public/index.php` is publicly accessible. All `app/`, `config/`, `routes/`, `storage/` directories are protected by `.htaccess` deny rules.

---

## 3. Naming Conventions

### 3.1 Files and Classes

| Type | Naming Pattern | Example |
|------|---------------|---------|
| Controller | `{Module}Controller.php` | `GuestController.php` |
| Service | `{Module}Service.php` | `GuestService.php` |
| Model | `{Module}Model.php` | `GuestModel.php` |
| View directory | `{module}/views/` | `Guest/views/` |
| Core class | `{Name}.php` | `Router.php` |
| Helper | `{function_name}.php` | `escape_html.php` |
| Config file | `{name}.php` | `database.php` |
| Route file | `{portal}.php` | `couple.php` |

### 3.2 Variables and Functions

- Variables: `snake_case` (`$guest_code`, `$workspace_id`)
- Functions: `snake_case` (`env()`, `config()`, `escape_html()`, `csrf_field()`)
- Constants: `SCREAMING_SNAKE_CASE` (`ROOT_PATH`, `APP_DEBUG`)
- Classes and methods: `PascalCase` / `camelCase` (`BaseController`, `render()`)

### 3.3 Database

- Table names: `snake_case`, plural (`guests`, `workspace_events`)
- Column names: `snake_case` (`created_at`, `guest_code`)
- Primary key: `id` (integer, auto-increment)
- Foreign keys: `{referenced_table_singular}_id` (`workspace_id`, `guest_id`)

### 3.4 Route URIs

- All lowercase, hyphen-separated: `/couple/guest-list`, `/admin/workspace/{id}`
- Named parameters use braces: `{id}`, `{slug}`, `{guest_code}`
- Portal prefix: `/couple/...`, `/crew/...`, `/admin/...`, `/guest/...`

---

## 4. Environment Configuration

*(REV-018)*

### 4.1 .env File

The `.env` file is the single source of all environment-specific values. It is never committed to version control. `.env.example` is committed and documents every required key.

### 4.2 Required Environment Keys

```ini
# Application
APP_NAME=Inveetaire
APP_ENV=local                    # local | staging | production
APP_DEBUG=true                   # true | false
APP_URL=http://localhost/inveetaire

# Database
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=inveetaire
DB_USER=inveetaire_app
DB_PASS=
DB_CHARSET=utf8mb4

# Session
SESSION_NAME=inveetaire_session
SESSION_TIMEOUT_COUPLE=86400     # 24 hours (seconds)
SESSION_TIMEOUT_ADMIN=28800      # 8 hours (seconds)
SESSION_TIMEOUT_CREW=43200       # 12 hours (seconds)
SESSION_INACTIVITY_TIMEOUT=7200  # 2 hours — applies to all portals (REV-018)

# Storage
STORAGE_UPLOADS=storage/uploads
STORAGE_EXPORTS=storage/exports
STORAGE_LOGS=storage/logs
STORAGE_CACHE=storage/cache
```

> **REV-018**: `SESSION_INACTIVITY_TIMEOUT` is a required configuration key (7200 = 2 hours). It is separate from portal-specific timeout values and applies universally across all portals. It is formally documented in MIB AUTH-004.

### 4.3 Config Files

Config files live in `config/` and return PHP arrays. They are loaded by `config()` on first access and cached per-request via static variable.

```php
// config/database.php — example
return [
    'host'     => $_ENV['DB_HOST'] ?? '127.0.0.1',
    'port'     => $_ENV['DB_PORT'] ?? '3306',
    'name'     => $_ENV['DB_NAME'] ?? '',
    'user'     => $_ENV['DB_USER'] ?? '',
    'pass'     => $_ENV['DB_PASS'] ?? '',
    'charset'  => $_ENV['DB_CHARSET'] ?? 'utf8mb4',
    'options'  => [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES   => false,
    ],
];
```

---

## 5. Front Controller, Bootstrap & Autoloader

*(REV-010 — execution order formalized)*

### 5.1 Single Entry Point

All HTTP requests arrive at `public/index.php`. No other PHP file is directly accessible from the web.

```php
// public/index.php
define('ROOT_PATH', dirname(__DIR__));
require ROOT_PATH . '/app/core/Bootstrap.php';
$bootstrap = new Bootstrap();
$bootstrap->init();
```

### 5.2 Bootstrap Sequence

`Bootstrap::init()` executes the following steps in exact order:

1. Define `ROOT_PATH`
2. Load `.env` file into `$_ENV` (minimal line-parser, no library)
3. Load helper functions: `env`, `config`, `csrf_field`, `escape_html`
4. Register the PSR-4 autoloader
5. Configure PHP error reporting (`APP_DEBUG`)
6. Register global error and exception handlers (`ErrorHandler`)
7. Start session (`Session::start()`)
8. Dispatch request → `Application::handleRequest()` → `Router::dispatch()`

> **RULE**: `ROOT_PATH` is defined in `public/index.php`. Helpers loaded in step 3 cannot reference `ROOT_PATH` — they must use `dirname(dirname(dirname(__FILE__)))` to resolve the project root. This is the correct and intentional pattern. See Section 10.4. *(REV-019)*

### 5.3 Autoloader

A minimal `spl_autoload_register` maps the `App\` namespace to `app/`. No Composer at this stage.

```
App\Core\Router        → app/core/Router.php
App\Modules\Guest\...  → app/modules/Guest/...
```

### 5.4 Sprint 0 INIT Execution Order

*(REV-010, REV-011)*

INIT task numbers are identifiers, not execution sequence. The dependency-safe order is:

```
INIT-001 → INIT-002 → INIT-005 → INIT-003 → INIT-004
                                      ↓
                                  INIT-006
                                      ↓
                                  INIT-007
                                      ↓
                                  INIT-008
                                      ↓
                                  INIT-012  ← validates Database.php (created in INIT-008)
                                      ↓
                                  INIT-009
                                      ↓
                                  INIT-010
                                      ↓
                                  INIT-011
```

> Dependency order always takes precedence over numeric task identifiers.

---

## 6. Error Handling & Logging

*(REV-015)*

### 6.1 ErrorHandler

`ErrorHandler` registers PHP `set_error_handler()` and `set_exception_handler()`. It routes to `Logger` or to a debug output based on `APP_DEBUG`.

- `APP_DEBUG=true` → stack trace displayed in browser
- `APP_DEBUG=false` → `app/views/errors/500.php` shown; full trace written to log

### 6.2 Logger

`Logger.php` is implemented to **full production specification** in INIT-006. No further finalization is required in any later sprint. *(REV-015)*

**Log levels**: `ERROR`, `WARNING`, `INFO`

**Log format**: `[UTC timestamp] [LEVEL] [URL] [User/Role] Message`

**Log path**: resolved from `config('storage.logs')` with `ROOT_PATH` fallback.

**Thread safety**: writes protected with `LOCK_EX`.

**Failure fallback**: falls back to PHP's native `error_log()` — Logger never throws.

Log files are created at: `storage/logs/app-YYYY-MM-DD.log`

### 6.3 Error Pages

- `app/views/errors/404.php` — Page not found
- `app/views/errors/403.php` — Access denied
- `app/views/errors/500.php` — Something went wrong
- `app/views/errors/maintenance.php` — Maintenance mode

---

## 7. Routing

### 7.1 Router

`Router.php` handles URL matching, named parameter extraction, middleware resolution, and controller dispatch.

Supported HTTP methods: `GET`, `POST` only (no PUT/DELETE at MVP).

### 7.2 Route Registration

Route files in `routes/` use a global `$router` instance. They are loaded by `Router::loadRoutes()` during bootstrap.

```php
// routes/couple.php (example)
$router->get('/couple/dashboard', 'Couple\DashboardController@index', ['auth', 'workspace']);
$router->post('/couple/guest/add', 'Couple\GuestController@store', ['auth', 'workspace', 'csrf']);
```

### 7.3 Named Parameters

Defined with braces: `{id}`, `{slug}`, `{guest_code}`. Extracted from the URI and passed as an array to the controller method.

### 7.4 Route Files

| File | Portal |
|------|--------|
| `routes/public.php` | Public + guest invitation routes |
| `routes/couple.php` | Couple portal |
| `routes/crew.php` | Crew portal |
| `routes/admin.php` | Super Admin portal |
| `routes/support.php` | Support Mode overlay |

Route files are skeleton files in Sprint 0 — populated when their respective modules are built.

---

## 8. Session Management & CSRF

*(No changes from v1.0 except SESSION_INACTIVITY_TIMEOUT — see Section 4.2)*

### 8.1 Session

`Session::start()` is called in the final step of `Bootstrap::init()`. Cookie settings:

```
name:             inveetaire_session
cookie_httponly:  true
cookie_secure:    true (HTTPS in production)
cookie_samesite:  Strict
save_path:        storage/sessions/
```

Session timeout values are read from `config/session.php` which reads from `.env` keys documented in Section 4.2.

### 8.2 CSRF

`Csrf::getToken()` generates a 64-character hex token once per session (`$_SESSION['csrf_token']`). It is regenerated after each successful form submission. Validation uses `hash_equals()` for timing-safe comparison.

The `csrf_field()` helper outputs a hidden input:

```php
echo csrf_field();
// <input type="hidden" name="_csrf" value="...">
```

CSRF validation on POST requests is performed by `CsrfMiddleware` (Sprint 1 — Core Utilities). *(REV-013)*

### 8.3 Flash Messages

`FlashMessage::set('success', 'Guest added.')` stores a message in `$_SESSION['flash']`. On the next request, `FlashMessage::pull()` reads and atomically clears it. The layout renders the flash component which calls `pull()`.

---

## 9. View System

*(REV-015, REV-016, REV-017)*

### 9.1 View Rendering Mechanism

`BaseController::render($view, $data)` implements a two-pass buffered rendering model:

1. Extract `$data` into local variables
2. Include the module view file → captured into output buffer
3. Buffer assigned to `$content`
4. Layout file wraps `$content` → final HTML sent to browser

```php
// BaseController::render() — pseudocode
public function render(string $view, array $data = []): void
{
    extract($data);
    ob_start();
    include ROOT_PATH . '/app/modules/' . $view . '.php';
    $content = ob_get_clean();
    include ROOT_PATH . '/app/views/layouts/' . $this->layout . '.php';
}
```

### 9.2 Output Escaping

**All output in views, layouts, and components must use `escape_html()`.**

No raw `echo $variable` is ever permitted.

```php
// Correct
echo escape_html($guest['name']);
echo escape_html($event['title']);

// FORBIDDEN
echo $guest['name'];
echo $event['title'];
```

`escape_html()` is a thin wrapper around `htmlspecialchars()`:

```php
function escape_html(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
```

It lives in `app/helpers/escape_html.php` and is loaded by `Bootstrap::loadHelpers()`. *(REV-016)*

### 9.3 Portal Layouts

Four portal layout shells exist in `app/views/layouts/`. There is no generic `app.php` layout. *(REV-017)*

| Layout File | Portal |
|-------------|--------|
| `layouts/couple.php` | Couple Portal |
| `layouts/admin.php` | Super Admin Portal |
| `layouts/crew.php` | Crew Portal |
| `layouts/public.php` | Public / Guest Invitation |

`BaseController` has a default `$layout = 'app'` which acts as a no-layout fallback. Every portal controller must declare its layout explicitly: *(REV-017)*

```php
// In a portal controller:
protected string $layout = 'couple';  // or 'admin', 'crew', 'public'
```

Failure to set `$layout` results in the `app` fallback — no layout wrapping. This will produce unstyled output and should never occur in production portal controllers.

### 9.4 Layout Structure

Each layout file:

1. Opens the HTML document and loads CSS/JS assets
2. Renders the navigation component (`nav_couple.php`, etc.)
3. Renders flash messages via `FlashMessage::pull()`
4. Outputs `$content` (the view's rendered HTML)
5. Closes the document

### 9.5 Components

| Component | Purpose |
|-----------|---------|
| `components/flash.php` | Flash message banner |
| `components/nav_couple.php` | Couple portal navigation |
| `components/pagination.php` | Pagination links |

Components receive data as variables. They never query the database or call services.

---

## 10. Helper Infrastructure

*(REV-019)*

### 10.1 Global Helpers

All helpers are plain PHP functions — no class, no namespace, no instantiation. Each helper file is guarded by `function_exists()` to prevent redeclaration errors on repeated includes.

| Helper | File | Purpose |
|--------|------|---------|
| `env()` | `app/helpers/env.php` | Read `.env` variable with typed coercion and fallback |
| `config()` | `app/helpers/config.php` | Load config array value by dot-notation key |
| `csrf_field()` | `app/helpers/csrf_field.php` | Output CSRF hidden input HTML |
| `escape_html()` | `app/helpers/escape_html.php` | Escape output for HTML context |

### 10.2 env() Helper

```php
env(string $key, mixed $default = null): mixed
```

Reads from `$_ENV`. Applies typed coercion:

- `"true"` / `"false"` → `bool`
- `"null"` → `null`
- Numeric strings without leading zeros → `int` or `float`
- Leading-zero strings preserved as strings (e.g. `"007"`)

### 10.3 config() Helper

```php
config(string $key, mixed $default = null): mixed
```

Resolves dot-notation keys: `config('database.host')` loads `config/database.php` and returns the `host` key. Config files are loaded once per request and cached via static variable.

### 10.4 Project Root Resolution Before Bootstrap

*(REV-019)*

`ROOT_PATH` is defined in `public/index.php`. Helper files loaded during `Bootstrap::loadHelpers()` are required before `ROOT_PATH` is available to them as a constant.

**Correct pattern:**

```php
// In app/helpers/config.php
$root = dirname(dirname(dirname(__FILE__)));
// __FILE__ = app/helpers/config.php
// dirname x1 = app/helpers/
// dirname x2 = app/
// dirname x3 = project root
```

This pattern is the correct and intentional approach for any helper that must resolve a path before Bootstrap completes.

### 10.5 escape_html() Helper

```php
escape_html(string $value): string
```

A single-line wrapper:

```php
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
```

Approved prerequisite for INIT-011. Loaded by `Bootstrap::loadHelpers()`. *(REV-016)*

---

## 11. Coding Standards

### 11.1 PHP Standards

- PHP 7.4 minimum compatibility
- Strict types declared where beneficial: `declare(strict_types=1);`
- PSR-4 autoloading via custom autoloader
- No external Composer packages in core framework
- No Dependency Injection container
- No Reflection API

### 11.2 Forbidden Patterns

The following patterns are forbidden in all Inveetaire code:

```php
// FORBIDDEN — SQL in controllers
$result = $pdo->query("SELECT * FROM guests WHERE id = {$id}");

// FORBIDDEN — Raw echo in views
echo $guest['name'];

// FORBIDDEN — Business logic in controllers
if ($guest['rsvp_status'] === 'confirmed' && $guest['dietary_notes'] !== '') {
    // ... this belongs in GuestService
}

// FORBIDDEN — Cross-module model access
$guestData = (new GuestModel())->find(...); // from VendorController

// FORBIDDEN — Hardcoded workspace IDs
$guests = $model->findAll('guests', ['workspace_id' => 1]);

// FORBIDDEN — SELECT *
$guests = $pdo->query("SELECT * FROM guests");
```

### 11.3 Required Patterns

```php
// REQUIRED — Parameterized queries only
$guests = $this->db->query('SELECT id, name FROM guests WHERE workspace_id = ?', [$workspace_id]);

// REQUIRED — Output escaping
echo escape_html($guest['name']);

// REQUIRED — All writes use transactions
$this->db->beginTransaction();
// ... writes
$this->db->commit();

// REQUIRED — Config from config files
$timeout = config('session.timeout.couple');
```

### 11.4 Function and Method Rules

- Maximum controller method length: 20 lines
- Maximum function length: 40 lines
- Single responsibility per function
- Early return preferred over deep nesting
- No commented-out code
- No `var_dump()`, `print_r()`, `die()` outside of controlled debug blocks
- No TODO comments before merge

---

## 12. Database Layer

*(REV-011, REV-020)*

### 12.1 Database Singleton

`Database::getInstance()` returns a single PDO connection per request lifecycle.

PDO configuration:

```php
PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
PDO::ATTR_EMULATE_PREPARES   => false   // real prepared statements
charset:                     utf8mb4
```

`Database.php` is implemented to production specification in **INIT-008**. INIT-012 validates the live connection against actual MariaDB credentials — it does not re-implement or extend this class. *(REV-011)*

### 12.2 BaseModel Core Data Access Primitives

*(REV-020)*

Every module model extends `BaseModel`. The following methods are available to all module models:

| Method | Signature | Returns |
|--------|-----------|---------|
| `query` | `query(string $sql, array $params): array` | Result rows (array of arrays) |
| `find` | `find(string $table, array $conditions): ?array` | Single row or null |
| `findAll` | `findAll(string $table, array $conditions): array` | Multiple rows |
| `insert` | `insert(string $table, array $data): int` | Last insert ID |
| `update` | `update(string $table, array $data, array $conditions): void` | — |
| `count` | `count(string $table, array $conditions): int` | Row count *(REV-020)* |

`count()` is a Core Data Access Primitive. It returns an integer and is available on all models.

### 12.3 Query Rules

- All queries use parameterized PDO — no string concatenation
- All queries filter by `workspace_id` where applicable
- All writes that touch multiple tables use PDO transactions
- Soft deletes are respected (`deleted_at IS NULL` where applicable)
- Never use `SELECT *` — always name columns explicitly

### 12.4 Database Validation (INIT-012)

INIT-012 is a validation task. It does not create any files. It validates:

1. Connection established from `.env` credentials
2. `SELECT 1` returns result `1`
3. Charset is `utf8mb4`
4. `Database::getInstance()` returns same instance on repeated calls
5. Real prepared statements execute correctly
6. Bad credentials → exception caught → logged → 500 page shown (no raw PHP error)

> **Pre-requisite (manual):** The `inveetaire` database and `inveetaire_app` user must be provisioned before INIT-012 criteria can be verified. Schema migration per PDD v1.0 Ch.13 must be complete.

---

## 13. Middleware Architecture

*(REV-013)*

### 13.1 MiddlewarePipeline

`MiddlewarePipeline.php` is the only middleware artifact created during **Sprint 0 (INIT-009)**. It implements the middleware execution engine.

Pattern: **Onion/decorator** — middleware wraps are built inside-out. The first class registered executes first.

Each middleware must expose:

```php
public function handle(Request $request, callable $next): void
```

A middleware may short-circuit the chain by not calling `$next`. `MiddlewarePipeline.php` has no session, database, auth, or CSRF dependencies.

### 13.2 Concrete Middleware — Deferred to Implementing Sprints

*(REV-013)*

Concrete middleware classes must NOT be created as stubs before their implementing sprint. Creating stubs that reference undefined Auth structures produces dead code.

`app/middleware/` contains only `.gitkeep` after Sprint 0.

| Middleware File | Implementing Sprint |
|----------------|-------------------|
| `CsrfMiddleware.php` | Sprint 1 — Core Utilities |
| `AuthMiddleware.php` | Sprint 2 — Authentication |
| `RoleMiddleware.php` | Sprint 2 — Authentication |
| `WorkspaceMiddleware.php` | Sprint 3 — Workspace |
| `GuestMiddleware.php` | Sprint 4/5 — Guest |
| `SupportMiddleware.php` | Sprint 13 — Support Mode |

### 13.3 Middleware Registration

Routes register middleware by name:

```php
$router->get('/couple/dashboard', 'Couple\DashboardController@index', ['auth', 'workspace']);
```

The `Router` resolves names to class instances and passes them to `MiddlewarePipeline`.

---

## 14. Module Structure

Every module follows the same internal structure. No exceptions.

```
app/modules/{ModuleName}/
├── {ModuleName}Controller.php
├── {ModuleName}Service.php
├── {ModuleName}Model.php
└── views/
    ├── index.php
    ├── show.php
    ├── create.php
    └── edit.php
```

### 14.1 Controller Responsibilities

1. Receive and validate input (Request object)
2. Call the module Service with validated data
3. Return a response (render, redirect, or JSON)

No controller method calls `BaseModel` directly. All data access goes through the module Service.

### 14.2 Service Responsibilities

- Contain all business logic for the module
- Call the module Model for data access
- Validate business rules (as distinct from input validation in the controller)
- Call other module services via their public interface only

### 14.3 Model Responsibilities

- Extend `BaseModel`
- Provide module-specific named query methods that call the inherited primitives
- Always filter by `workspace_id`
- Always exclude soft-deleted rows (`deleted_at IS NULL`)
- Return arrays — never objects

---

## 15. Appendix: CLI Validation Test Scaffold

*(REV-022)*

During automated CLI testing of framework components, `Bootstrap::init()` writes session headers and other output before any test echo statement. This produces false-positive "headers already sent" PHP warnings in CLI contexts.

**Correct CLI test scaffold:**

```php
<?php
// cli_validate_something.php

ob_start();                    // 1. Begin output buffering
$bootstrap = new Bootstrap();
$bootstrap->init();            // 2. Init (headers/output captured)
ob_end_clean();                // 3. Discard bootstrap output

// 4. Safe to echo from here
echo "Testing Database connection...\n";
$db = Database::getInstance();
$result = $db->query('SELECT 1 AS result', []);
echo $result[0]['result'] === 1 ? "PASS\n" : "FAIL\n";
```

This pattern must be used in all PHP CLI validation scripts that call Bootstrap. Omitting it produces spurious warnings that do not reflect application behaviour.

---

## Synchronization Summary

### Chapters Modified

| Chapter | Change Type | Revision |
|---------|-------------|---------|
| Header | Version, change history, scope note | — |
| 4. Configuration | `SESSION_INACTIVITY_TIMEOUT` added to required env keys | REV-018 |
| 5. Front Controller | INIT execution order diagram added; `ROOT_PATH` / helper load note | REV-010, REV-019 |
| 6. Error Handling | Logger finalization note removed; Logger is production-complete in INIT-006 | REV-015 |
| 9. View System | `escape_html()` section added; portal layout override documented; no generic `app` layout clarified | REV-015, REV-016, REV-017 |
| 10. Helper Infrastructure | `__FILE__`-based root resolution pattern documented; `escape_html()` added to helper table | REV-016, REV-019 |
| 12. Database Layer | `count()` added to BaseModel primitives; INIT-012 reclassified as validation-only | REV-011, REV-020 |
| 13. Middleware | Chapter rewritten to reflect Sprint 0 scope: only `MiddlewarePipeline.php`; concrete middleware deferred | REV-013 |
| 15. Appendix (new) | CLI validation test scaffold documented | REV-022 |

### Revisions Applied

| Revision | Description |
|----------|-------------|
| REV-010 | INIT execution order formalized in Section 5.4 |
| REV-011 | INIT-012 reclassified as validation-only in Section 12.4; `Database.php` ownership clarified as INIT-008 |
| REV-013 | Chapter 13 rewritten: Sprint 0 scope = `MiddlewarePipeline.php` only; six concrete middleware deferred |
| REV-015 | Logger finalization note removed; Logger is production-complete after INIT-006 |
| REV-016 | `escape_html()` added to View System (Section 9.2), Helper table (Section 10.1), and Helper details (Section 10.5) |
| REV-017 | Portal layout override requirement documented in Section 9.3; no generic `app.php` layout clarified |
| REV-018 | `SESSION_INACTIVITY_TIMEOUT` added to Section 4.2 required env keys |
| REV-019 | `dirname(dirname(dirname(__FILE__)))` root resolution pattern documented in Sections 5.2 and 10.4 |
| REV-020 | `count()` added to BaseModel Core Data Access Primitives in Section 12.2 |
| REV-022 | CLI Validation Test Scaffold documented in Section 15 (Appendix) |
| REV-023 | Implementation authority hierarchy referenced in document header |

### Clarifications Added

- Logger is production-complete after INIT-006 — no later "finalization" task exists
- `Database.php` is created in INIT-008; INIT-012 only validates it against a live connection
- `app/middleware/` is empty (`.gitkeep`) after Sprint 0
- `BaseController::$layout = 'app'` is a no-layout fallback — portal controllers must override it
- Helper functions require `dirname(dirname(dirname(__FILE__)))` for root resolution before `ROOT_PATH` is available

### Architectural Corrections

- INIT-009 scope reduced to `MiddlewarePipeline.php` only (six concrete middleware stubs removed)
- INIT-012 is a validation task only — no new files
- `count()` formalized as a BaseModel Core Data Access Primitive

---

*Document produced: 2026-07-01*
*Based on: DESIGN_REVISION_PACK_v2.0*
*Sprint 0 status: Complete*
