Most small companies in Dubai run HR on spreadsheets until the spreadsheets fall apart. Attendance lives in one file, leave balances in another, and payroll is a monthly argument. I rebuilt that mess as a proper HR Management System (HRMS) with Laravel, using Filament v5 for the admin side and Livewire for a reactive employee portal. Here is how the pieces fit together โ and the decisions that mattered more than the code.
Why Laravel and Filament for an HRMS
An HRMS is mostly CRUD with sharp edges: employees, departments, attendance, leave, and payroll, all wrapped in strict permissions. Filament gives you a production-grade admin panel โ resources, forms, tables, and policies โ in a fraction of the time, so you spend your effort on the rules that actually matter to the business instead of rebuilding tables and forms.
The architecture splits cleanly into two areas:
- Admin panel (Filament): HR managers manage employees, departments, payroll, and leave approvals.
- Employee portal (Livewire): staff clock in/out, request leave, and update their own profile.
I had previously built the same system in Python Flask with MongoDB, so this was a deliberate rebuild โ and the comparison was instructive. Flask gives you freedom and makes you pay for it in glue code: every admin table, every form, every permission check is yours to write. Filament turned weeks of that work into resource classes measured in hours. When the framework's conventions match the problem โ and admin-heavy CRUD is exactly that problem โ fighting for flexibility you won't use is a bad trade.
Role-based access from day one
An HR system lives or dies on permissions. I separated the two areas into distinct guards so an employee can never reach admin routes:
// config/auth.php โ two guards, two providers
'guards' => [
'admin' => ['driver' => 'session', 'provider' => 'admins'],
'employee' => ['driver' => 'session', 'provider' => 'employees'],
],
// Admin and employee live behind separate auth guards
Route::middleware('auth:admin')->group(function () {
// Filament admin panel
});
Route::middleware('auth:employee')->group(function () {
// Livewire employee portal
});
Separate guards โ not just roles on one users table โ means a compromised employee login can't be escalated into the admin panel by flipping a column. Inside the admin panel, Filament policies handle record-level access: a department manager sees their department's leave requests, not the whole company's. The employee portal is even stricter โ every query is scoped to the authenticated employee, so there is no URL you can edit to see a colleague's salary.
On top of the guards, both login areas use a short PIN as a second step. It's lightweight, but for an HR system holding salaries and documents, a second factor is the difference between "password leaked" and "data leaked".
Attendance and leave that managers trust
The features that earn their keep are the boring ones done correctly:
- Attendance: check-in/check-out with timestamps, so payroll is based on real data, not memory.
- Leave requests: employees submit, managers approve, and balances update automatically.
- Audit trail: every approval is logged, which ends the "who approved this?" arguments.
Leave balances deserve a design note, because this is where HR systems usually rot: I never store a balance as a mutable number. The balance is computed from the ledger โ accruals in, approved leave out:
public function leaveBalance(Employee $employee, LeaveType $type): float
{
return $employee->leaveEntries()
->where('leave_type_id', $type->id)
->sum('days'); // accruals positive, taken leave negative
}
A stored balance and a history that disagree with each other is an unwinnable argument. A ledger can always be replayed, audited, and corrected with a compensating entry โ the same reason accountants don't use erasers.
Attendance has its own edge cases worth handling explicitly: the double check-in (idempotency again โ a second tap within a minute is ignored), the forgotten check-out (flagged for a manager instead of silently assuming 18:00), and timezone discipline โ everything is stored in UTC and rendered in Gulf Standard Time, so DST-free UAE never bites you when someone opens the portal while travelling.
Self-hosted, on purpose
Because it is self-hosted โ it runs on my own Docker infrastructure โ the company owns its HR data outright: no per-seat SaaS fees, no salary data sitting on someone else's server, and no feature disappearing behind a new pricing tier. For a small company the running cost is effectively the electricity bill of one small box. Backups are automated dumps shipped off the machine on a schedule, tested by actually restoring them โ an untested backup is a hope, not a plan.
Common pitfalls
- One users table for everyone. Mixing admins and employees in one table with a
rolecolumn feels simpler until the first authorization bug. Separate guards cost an hour and remove a whole class of exploits. - Storing computed balances. Any number that can be derived from history should be derived from history.
- Building payroll first. Payroll is the most complex module and depends on attendance and leave being right. Ship the foundations first; payroll calculations built on bad attendance data are confidently wrong.
- Ignoring the audit trail until launch. Retrofitting "who changed what, when" is miserable. Log it from the first migration.
- Making the employee portal do too much. Every extra feature is another permission surface. Small portal, small attack surface.
Lessons from shipping it
- Model permissions before features. Retrofitting access control into an HRMS is painful; design the guards first.
- Keep the employee portal tiny. Staff need three things โ clock in, request leave, see their profile. Resist scope creep.
- Filament is a force multiplier. The admin panel that would have taken weeks took days, and it looks better than what I would have hand-built.
A focused Laravel HRMS beats a generic SaaS for a small team: it fits the company's actual process, costs nothing per user, and keeps the data in-house.
FAQ
Why not just use an off-the-shelf HR SaaS?
Can Filament handle payroll calculations?
How long did the rebuild take?
Does self-hosting an HRMS meet UAE data expectations?
Need a custom HRMS or a Laravel build in Dubai, UAE? Get in touch.