A multi-tenant maintenance SaaS for non-profit facilities, engineered as a Laravel 13 application that unifies the work-order lifecycle — report, assign, approve, resolve — with a persistent asset registry, field-observation tooling, and complete audit trails over a containerized MySQL runtime.
| Domain | Non-profit facilities maintenance |
| Architecture | Laravel MVC with a service layer — containerized Nginx + PHP-FPM |
| Backend | PHP 8.3 · Laravel 13 · Eloquent ORM |
| Frontend | Blade + Tailwind CSS 4 · Arabic-first RTL · Vite |
| Database | MySQL 8.0 — 18 migrations, 10 seeders |
| RBAC | 5 roles · 49 permissions · per-route CheckPermission middleware |
| Security | Session auth, private file storage (no public symlink), full audit trails |
| Quality | PHPUnit 12 (Feature + Unit) · Laravel Pint in CI |
- Non-profit facility operators report, assign, approve and resolve maintenance work orders against a persistent asset registry.
- Field teams record observations and before/after photos tied to the asset and to an open maintenance report.
- Leadership tracks projects and expenses scoped to facilities, and reviews a read-only audit log of every mutation.
- Every state change is recorded by
AuditLogServicewith actor, module, and before/after values. - A containerized runtime (single app image + MySQL) makes deployment reproducible with a migration-and-seed entrypoint.
flowchart LR
Client[Browser - RTL Blade UI] --> Nginx[Nginx]
Nginx -->|FastCGI| FPM[PHP-FPM Laravel]
FPM --> Routes[Routes + CheckPermission RBAC gates]
Routes --> Controllers[Controllers]
Controllers --> Services[AuditLogService]
Controllers --> Models[Eloquent - 13 entities]
Models --> DB[(MySQL 8.0)]
Start[start.sh migrate + seed + cache] --> DB
Files[storage/app/private uploads] -->|files.download route only| Controllers
CI[GitHub Actions] -. PHPUnit + Pint .-> FPM
- Private file handling — all uploads live under
storage/app/private/and are served only through the authenticatedfiles.download/{path}route; no public symlink. - RBAC at the router — 49 granular permissions (7 modules × 7 actions) enforced inline on each route in
routes/web.php. - Complete audit trails —
AuditLogServicerecords actor, module, and before/after values for every mutation. - Schema as code — 18 migrations and 10 idempotent seeders define the whole data model.
- Production entrypoint —
start.shpolls DB readiness, runs migrations, seeds when empty, and caches config/routes/views.
FacilityOps-Care is a conventional Laravel MVC application with domain logic partitioned into a thin service layer, an Eloquent model layer over 13 entities, and a route map in which every state-changing endpoint is guarded by both session authentication and a per-route permission gate. The runtime is fully containerized: a single php:8.3-fpm image embeds Nginx and PHP-FPM behind start.sh, which performs database readiness polling, migrations, conditional seeding, and environment-aware config/route/view caching before serving traffic.
- Laravel MVC — controllers orchestrate requests; views render an RTL Blade layout (Arabic-first) with no public storage symlink for uploads.
- Service layer —
AuditLogServicerecords every mutation with actor, module, and old/new values. - Schema as code — 18 migrations define the MySQL 8.0 schema; 10 seeders populate roles, permissions, regions, and gated demo data (idempotent against existing users).
- Containerized runtime — Nginx reverse-proxies FastCGI to PHP-FPM inside one image; MySQL runs as a separate health-checked Compose service.
- Quality gates — PHPUnit 12 suites (Feature + Unit) executed via
php artisan test; Laravel Pint enforces style in CI.
- Asset registry —
Mosquemodel with soft deletes; fields for region, city, neighborhood, geo-coordinates (decimal:7), capacity, imam/muezzin details, construction year, and ownership type; one-to-many attachments. - Work-order lifecycle —
MaintenanceReportwith category, priority, and a full status state machine; dedicatedreports.approve/reports.rejectendpoints for approver/rejecter workflow; report attachments bound to the originating mosque. - Field-visit tooling —
FieldVisitrecords with purpose, notes, and before/afterVisitPhotoattachments, optionally associated with an open maintenance report. - Project & expense tracking —
Projectlinked to multiple mosques via theproject_mosquespivot;Expenserecords scoped to mosques and projects. - RBAC at the router — 13 Eloquent models, 5 roles, 49 granular permissions enforced by the custom
CheckPermissionmiddleware (e.g.mosques.view,reports.approve) declared inline on each route inroutes/web.php. - Private file serving — all uploads persist under
storage/app/private/; the sole access path is the authenticatedfiles.download/{path}route — no public symlink. - Observability —
AuditLogControllerexposes a read-only log surface. - Frontend build — Blade + Tailwind CSS 4, bundled by Vite 8 via
laravel-vite-plugin;npm run buildemits versioned assets topublic/build.
| Layer | Technology | Specification |
|---|---|---|
| Language runtime | PHP | 8.3 (pdo_mysql, mbstring, gd, bcmath, zip, opcache) |
| Framework | Laravel | laravel/framework ^13.8, Eloquent ORM |
| Frontend | Blade + Tailwind CSS | Tailwind 4, laravel-vite-plugin ^3.1, Vite ^8 |
| Database | MySQL | 8.0, 18 migrations, 10 seeders |
| Containers | Docker Compose | php:8.3-fpm + Nginx (single app image); mysql:8.0 service with healthcheck; app exposed on 8000:80, DB on 3307:3306 |
| Reverse proxy | Nginx | try_files + FastCGI passthrough to 127.0.0.1:9000 |
| Init/entrypoint | start.sh |
DB readiness poll (30 attempts), migrate --force, seed-if-empty, env-aware config/route/view cache |
| Auth & RBAC | Laravel auth + CheckPermission |
Session auth middleware group; per-route permission gates |
| Services | app/Services |
AuditLogService |
| Testing | PHPUnit | ^12.5.12 via php artisan test |
| Style | Laravel Pint | ^1.27, enforced with --test in CI |
- PHP 8.3+ (with the extensions listed above), Composer 2.x
- Node.js 20+ and npm
- MySQL 8.0, or Docker with Docker Compose
composer install
cp .env.example .env
php artisan key:generate
# Configure DB_* variables in .env (DB_CONNECTION=mysql, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD)
php artisan migrate
npm install
npm run build
php artisan serve
# -> http://localhost:8000All secrets — APP_KEY, DB_PASSWORD, and every other credential — are supplied exclusively through .env (see .env.example). The repository never ships or tracks real credentials.
cp .env.example .env
# Set APP_KEY, DB_DATABASE, DB_USERNAME, DB_PASSWORD, MYSQL_ROOT_PASSWORD in .env before boot
docker compose up -d
# -> http://localhost:8000The app container's start.sh waits for MySQL health, runs php artisan migrate --force, seeds only when the database is empty, then caches config/routes/views in production before starting PHP-FPM and Nginx.
php artisan test # PHPUnit 12 (Feature + Unit)
vendor/bin/pint # Laravel Pint style fixer
vendor/bin/pint --test # Style compliance check (used by CI)- Environment-based secrets — every credential (encryption key, database password) is injected via
.envand consumed throughconfig/; Compose resolvesAPP_KEYandDB_*from the host environment, never from the repository. - Authentication at the boundary — all application routes sit inside the
authmiddleware group; theCheckPermissionmiddleware independently aborts with403on unauthorized route access. - Private storage by default — uploaded files live under
storage/app/private/and are reachable only through the authenticated download route; there is nopublic/storagesymlink. - Repository hygiene —
.gitignoreexcludes.env,.env.*.local, and.env.production;storage/*.key,node_modules,vendor, and build artifacts are untracked, anddocker-compose.ymlreferences only environment-variable placeholders.
MIT License — Copyright © 2026 Hamed Elaraby.