# 02 — Architecture

## 1. Stack

| Layer | Choice | Notes |
|---|---|---|
| Runtime | PHP 8.3, Laravel 12 | LTS-ish, typed, modern |
| DB | MySQL 8 (InnoDB, utf8mb4) | FULLTEXT for search; JSON columns sparingly |
| Cache/Queue | Redis + Horizon | All mail/PDF/Excel/reminders queued |
| Frontend | Blade + Alpine.js + chosen admin template, Vite | No SPA in v1 — fastest to ship, easiest to secure |
| Files | Local private disk → S3-compatible later (config swap) | via spatie/medialibrary |
| PDF / Excel | dompdf / maatwebsite-excel | Generated in queue jobs |
| Auth | Laravel session auth (staff + portal guards), TOTP 2FA | Sanctum only if a mobile/API client appears |

## 2. Application layering

```
HTTP Request
  → Route (name, middleware: auth, permission, throttle)
    → Controller (thin: authorize → delegate → respond)
      → FormRequest (validation + authorize())
      → Action (app/Actions/{Domain}/CreateMatter.php — one business operation,
                transactional, fires events)
        → Services (cross-cutting engines: LedgerService, NumberingService,
                    ReminderService, ConflictCheckService)
        → Models (relations, scopes, casts, accessors — no business workflows)
  ← View (Blade component-based) / redirect with flash / JSON
Async: Events → queued Listeners/Jobs (notifications, PDFs, digests)
```

Rules:
- Controllers never touch `DB::` directly and never exceed ~40 lines/method.
- Actions are the ONLY place multi-step business logic lives; each wraps its work
  in `DB::transaction()` and is unit-testable in isolation.
- Blade views receive prepared view-models/arrays — no query building in views.

## 3. Directory layout

```
app/
├── Actions/{Matters,Billing,Finance,Litigation,...}/
├── Enums/                  (MatterStatus, HearingOutcome, FeeType, …)
├── Http/
│   ├── Controllers/{App,Portal}/           (staff app, client portal)
│   ├── Middleware/
│   └── Requests/{Domain}/
├── Models/ (+ Models/Scopes/FirmScope.php, Models/Concerns/)
├── Policies/
├── Services/
├── Jobs/  ·  Events/  ·  Listeners/  ·  Notifications/
└── Support/ (helpers, value objects e.g. Money, Duration)

resources/views/
├── layouts/ (app, portal, print)
├── components/ (x-card, x-table, x-form.*, x-badge, x-modal, x-empty, x-kpi)
├── app/{module}/           (staff pages)
└── portal/                 (client pages)

routes/  web.php (staff)  ·  portal.php  ·  console.php
tests/   Feature/{Module}/  ·  Unit/
docs/    (this documentation)
```

Two route files → two middleware groups → two layouts. Clean separation of
the staff app and the client portal from day one.

## 4. Multi-tenancy posture

Single firm at launch, but every domain table carries `firm_id` with a global
`FirmScope` (applied via a `BelongsToFirm` trait) and a `firms` table exists from
migration #1. Cost now: near zero. Benefit: the product can be sold to the next
law firm without a rewrite. Cross-firm isolation is a standing test in every
module's suite.

## 5. Identifier & numbering strategy

- Primary keys: `id` BIGINT internal + `ulid` (unique, indexed) exposed in all
  URLs and route-model-binding (`getRouteKeyName() = 'ulid'`).
- Human numbers via `NumberingService`: per-firm, per-year, per-sequence-key,
  race-safe (dedicated `number_sequences` row locked `FOR UPDATE`):
  matters `MAT/2026/0001`, invoices `INV/2026/0001`, receipts `RCT/…`,
  claims `AFC/…`. Prefixes configurable in firm settings.

## 6. Performance & scalability

- **Indexes**: every FK; composite indexes on hot filters
  (`hearings(firm_id, hearing_date)`, `matters(firm_id, status)`,
  `invoices(firm_id, status, due_date)`). Defined in 03-DATA-MODEL.
- **N+1 ban**: `Model::preventLazyLoading(!app()->isProduction())` in
  AppServiceProvider; lists always eager-load.
- **Pagination everywhere**; server-side DataTables only if a table exceeds ~5k
  rows in practice.
- **Caching**: dashboard widgets cached 5 min (tagged, busted by domain events);
  reference data (courts, practice areas) cached forever, busted on write.
- **Queues**: emails, SMS, PDF/Excel generation, digests, reminder fan-out.
  Scheduled commands: `reminders:dispatch` (hourly), `digest:daily` (07:00),
  `matters:flag-stale` (weekly), `backup:run` (nightly).
- **Scaling path** (documented, not prematurely built): app is stateless
  (sessions in Redis) → horizontal app nodes behind a load balancer → managed
  MySQL with a read replica for reports → S3 for files. Nothing in the codebase
  may assume a single server (e.g., local temp files must be job-scoped).

## 7. Error handling & observability

- Domain exceptions (`app/Exceptions/Domain/*`, e.g. `TrustOverdrawException`)
  render as friendly flash errors; everything else → generic 500 page (no stack
  traces to users).
- Logging: daily channel + separate `audit` context; slow query log > 1s in
  production reviewed weekly.
- Health endpoint `/up` (Laravel built-in) for uptime monitoring.
- Sentry (or Flare) DSN slot in config — wire when available.

## 8. Environment & configuration

- `.env` only via `config/*` (never `env()` outside config).
- Feature flags in `config/features.php` (e.g. `payroll`, `sms`, `portal`) read
  through a `feature('sms')` helper — modules ship dark until switched on.
- Firm-editable settings (prefixes, VAT rate, reminder offsets, branding) in a
  `settings` key-value table with a typed `Settings` service, cached.
