The problem
Small companies run HR on spreadsheets until the spreadsheets break. OnBordly is the thing they move to: one place for employee records, leave, attendance, onboarding, roles and audit history, sold on Free, Premium and Enterprise plans. I co-founded it and lead its engineering, so the decisions below are ones I made and have to live with.
Decisions I would defend in a code review
Tenancy lives in the framework, not in my memory. Each company is a Jetstream Team. Every model that belongs to a company is scoped through the team relationship over a service/repository layer, so a query that forgets to filter by company is a type error in the service, not a data leak in production. The alternative, "remember to add where company_id", is how HR platforms end up on the news.
Plan limits live in one place. Billing runs on Laravel Cashier with three tiers. Rather than sprinkling if ($company->plan === 'premium') through controllers, every entitlement is a Laravel Pennant feature. A controller asks can this company do X; the answer, and the plan matrix behind it, is defined once. Changing what Premium includes is a one-file change and a test.
Roles are data, not code. RBAC is built on Spatie permissions with custom roles per company, so an HR lead can create a "payroll viewer" role without a deploy. Every privileged action writes an audit log entry with actor, subject and diff.
What shipped
Eight modules: employee management, leave, attendance, onboarding, RBAC with custom roles, audit logging, admin analytics and subscription billing with feature gating. Files go to S3; PHPUnit covers the billing and permission paths because those are the ones that cost money when wrong.
What I learned
Leading engineering on a young product is mostly about choosing which problems to make impossible rather than merely unlikely. Tenancy and entitlements are the two I chose. Everything else can be fixed on a Tuesday.