System Overview & Roles
Engineering Services is a multi-tenant SaaS platform for construction and engineering project management. A single deployment serves many independent business tenants, each isolated in their own workspace.
User Types
| user_type | Role | Scope | API prefix |
|---|---|---|---|
| super_admin | Platform administrator | Global — sees all tenants | /api/platform/* |
| admin | Business admin | Scoped to own business_id | /api/admin/* |
| staff | Business staff member | Scoped to own business_id | /api/admin/* |
| employee | Field employee | Own assignments only | /api/employee/* |
| contractor | Subcontractor | Own workspace only | /api/contractor/* |
| client | Client (project owner) | Own projects only | /api/client/* |
super_admin operates at platform level — no business_id filter applies. All other users are scoped and cannot cross tenant boundaries.
System Setup Order
Authentication & Super Admin Access
The platform uses Laravel Sanctum token auth. Every API call (except login) requires Authorization: Bearer {token}.
Super Admin Guard Logic
Seed Credentials
| Username | Password | Role | |
|---|---|---|---|
| superadmin | superadmin@engineering.test | Test@1234 | Platform super admin |
| admin | admin@engineering.test | Test@1234 | Business admin (tenant 1) |
| staff | staff@engineering.test | Test@1234 | Business staff (tenant 1) |
| client1 | client1@engineering.test | Test@1234 | Client portal |
| contractor1 | contractor1@engineering.test | Test@1234 | Contractor portal |
| employee1 | employee1@engineering.test | Test@1234 | Employee portal |
Subscription Plan Management
Plans define what a tenant business is allowed to do. They must be created before any tenant is invited, because tenant invite requires a valid active subscription_plan_id.
Plan Fields
| Field | Type | Meaning |
|---|---|---|
| name | string | Human-readable plan tier name (e.g. "Pro") |
| slug | string | URL-safe identifier; auto-generated from name if omitted |
| monthly_price | decimal | Billing amount per month in local currency |
| max_projects | integer | Max concurrent active projects; -1 = unlimited |
| max_locations | integer | Max business locations; -1 = unlimited |
| max_employees | integer | Max employee user accounts; -1 = unlimited |
| has_client_portal | boolean | Whether client portal access is included in plan |
| has_offline_sync | boolean | Whether offline data sync is included |
| is_active | boolean | Inactive plans cannot be assigned to new tenants |
Unlimited Limits Encoding
API input 5 → DB stored as 5 → API output 5
Transform: planLimitFromApi(n) = (n == -1) ? 0 : n
Reverse: planLimitToApi(n) = (n == 0) ? -1 : n
0 in the database always means "unlimited" — you cannot create a plan that allows exactly 0 projects. Use 1 as the minimum real limit.Slug Auto-generation
if slug is taken → slug = base + "-1", "-2", … until unique
Deletion Rules
- If no tenants assigned: plan is hard-deleted.
- If tenants are assigned: plan is deactivated (
is_active=false), and a422is returned with"error":"tenants_assigned". This protects existing tenants. - Deactivated plans are still shown in list but can no longer be assigned to new tenants.
Tenant Onboarding (Invite Flow)
The platform admin invites a new business tenant. Everything runs in a single database transaction.
Invite Flow
Subdomain Slug Generation
if base == "" → base = "tenant"
slug = base
while Business.exists(subdomain_slug=slug): slug = base + "-" + suffix++
Admin Username Generation
// "buildcorp-pakistan" → "buildcorppakistan_admin"
while User.exists(username=base): base = base + suffix++
Required Validation
| Field | Rule |
|---|---|
| business_name | required, string, max:255 |
| owner_name | required, string, max:255 |
| contact_email | required, valid email, max:255 |
| subscription_plan_id | required, integer, must exist + be active |
| contact_phone | optional, max:64 |
| create_admin_user | optional boolean, default true |
Tenant Lifecycle (Update & Suspension)
A tenant's status can be managed post-invite. No hard-delete is supported — suspension preserves all business data.
Status Values
| subscription_status | is_active | Meaning |
|---|---|---|
| active | true | Normal operating state |
| suspended | false | Manually suspended by platform admin |
| past_due | true | Payment overdue; access may be limited |
| cancelled | false | Subscription cancelled |
Audit Events on Update
- Any field change → writes
tenant_updatedevent listing changed fields - Transitioning
is_activefromtrue → falseadditionally writestenant_suspended - DELETE endpoint → always writes
tenant_suspended(soft delete)
Plan Change Rules
- New plan must exist and be
is_active=true - Downgrading a plan does not immediately enforce new limits — enforcement is the business admin's responsibility
- Upgrading is always allowed
Audit Event System
Every significant platform action writes an immutable row to t_platform_audit_events. These are exposed via /api/platform/notifications (the naming is UI-oriented, not email/push notifications).
Event Categories
| category | severity | Triggered by | metadata fields |
|---|---|---|---|
| tenant_created | info | POST /tenants | business_name, plan_name |
| tenant_updated | info | PUT /tenants/{id} (any field change) | summary of changed fields |
| tenant_suspended | action_taken | PUT (is_active false) or DELETE | business_name, suspension reason |
| plan_created | info | POST /subscription-plans | plan_name |
| plan_updated | info | PUT /subscription-plans/{id} or soft-delete on tenant-assigned plan | plan_name, change summary |
Event Code Format
// id=1 → "EVT-00001" | id=10047 → "EVT-10047"
Read / Unread
- Events start as
is_read: false - Clients poll
?is_read=falseto show a notification badge count - Mark individual or all read via PATCH endpoints
- Events can be permanently deleted (hard delete)
Dashboard KPIs
The GET /api/platform/dashboard response contains platform-wide aggregate metrics computed in real time.
KPI Formulas
active_tenant_count = COUNT(*) WHERE is_active = true
suspended_tenant_count = tenant_count - active_tenant_count
subscription_plan_count = COUNT(*) FROM t_subscription_plans WHERE is_active = true
plan_breakdown = SELECT plan_id, plan_name, COUNT(tenants) as tenant_count, is_active
FROM t_subscription_plans LEFT JOIN t_businesses
ORDER BY monthly_price ASC
recent_tenants = SELECT top 5 FROM t_businesses ORDER BY created_at DESC
Notes
subscription_plan_countcounts only active plans (inactive/deleted plans excluded)plan_breakdownincludes ALL plans (including inactive) to show full picturerecent_tenantsis limited to 5, ordered by signup date descending- No caching — values are live DB queries. Cache at the HTTP client for dashboards with high poll rates.
Permission Matrix
The super_admin role receives all platform permissions automatically. Business admin and lower roles do not receive any platform permission.
| Permission slug | Grants | Endpoints |
|---|---|---|
| tenants.view | Read tenant list and detail | GET /tenants, GET /tenants/{id} |
| tenants.manage | Invite, update, suspend tenants | POST, PUT, DELETE /tenants |
| subscription_plans.view | Read plan list and detail | GET /subscription-plans, GET /subscription-plans/{id} |
| subscription_plans.manage | Create, update, delete plans | POST, PUT, DELETE /subscription-plans |
| platform_notifications.view | Read audit event log | GET /notifications, GET /notifications/{id} |
| platform_notifications.manage | Mark read, delete events | PATCH .../read, PATCH .../read-all, DELETE /notifications/{id} |
tenants.manage, a request fails if the user's user_type is not super_admin. Both checks are always applied. The permission check is secondary — the super_admin check comes first.
Error Reference
| HTTP | error field | Meaning | Typical cause |
|---|---|---|---|
| 401 | — | Unauthenticated | Missing, expired, or revoked Sanctum token |
| 403 | — | Forbidden | user_type != super_admin, or missing permission slug |
| 404 | — | Not found | Resource ID does not exist |
| 422 | Validation errors object | Validation failed | Missing required field, type mismatch, unique violation |
| 422 | "tenants_assigned" | Plan has tenants | DELETE /subscription-plans/{id} when tenants exist (plan deactivated instead) |
| 422 | "invalid_id" | ID ≤ 0 | GET /notifications/0 or negative ID |
| 503 | — | OpenAPI spec not generated | GET /openapi.json before artisan openapi:generate has been run |
Validation Error Shape
"msg": "Validation failed",
"error": {
"contact_email": ["The contact email field must be a valid email address."]
}