πŸ”‘ Platform Admin β€” Business Guide Super Admin Console Β· Multi-Tenant SaaS Operator
πŸ›οΈ

Platform Architecture & Super Admin Role

PLT-001

The Platform Admin (also called Super Admin) is the SaaS operator β€” the entity that runs the multi-tenant Engineering Services platform. While individual engineering businesses (tenants) each operate their own isolated portal environment, the Platform Admin sits above all tenants and has unrestricted visibility and control across every Business record, subscription plan, and audit event in the system.

Each "tenant" represents one engineering business that has subscribed to the platform. Tenants operate through four role-specific portals (Admin, Employee, Client, Contractor). The Platform Admin manages the tenants themselves β€” provisioning, suspending, upgrading, and monitoring them β€” but does not participate in their day-to-day operations.

πŸ”‘ Platform Admin user_type = platform_admin rejectUnlessSuperAdmin() guard Β· /api/platform/* πŸ— Al-Noor Engineering Plan: Starter Β· is_active: true subdomain: al-noor-engineering πŸ— City Build Co. Plan: Professional Β· is_active: true subdomain: city-build-co πŸ— Apex Contractors Plan: Enterprise Β· is_active: true subdomain: apex-contractors Admin Β· Employee Β· Client Β· Contractor Isolated tenant portals Admin Β· Employee Β· Client Β· Contractor Isolated tenant portals Admin Β· Employee Β· Client Β· Contractor Isolated tenant portals Platform Admin Capabilities 🏒 Tenant CRUD Onboard businesses Suspend / reactivate Change subscription Manage admin users Soft-deactivate only Audit logged permission: tenants.manage πŸ“¦ Subscription Plans Create / update plans Set limits per plan max_projects/employees has_client_portal flag Deactivate if tenants Hard delete if empty permission: plans.manage πŸ“Š Dashboard KPIs tenant_count total active vs suspended plan_breakdown subscription_plan_count recent_tenants Γ— 5 Real-time, no cache GET /api/platform/dashboard πŸ“‹ Audit Log PlatformAuditEvent All platform operations event_code + category actor: who triggered Mark read / bulk read Hard delete allowed permission: notifications.*

Platform Admin vs Tenant Admin

DimensionPlatform Admin (Super Admin)Tenant Admin
ScopeAll tenants, all plans, all audit eventsSingle tenant's own data only
user_typeplatform_adminadmin
Auth guardrejectUnlessSuperAdmin()rejectUnlessPortalUserType()
Route prefix/api/platform/*/api/* scoped to business
Business isolationNone β€” sees all businessesStrict β€” business_id bound
Tenant CRUDYes β€” creates & manages tenantsNo
Subscription plansFull CRUDRead-only (own plan)
Audit logPlatform-level audit eventsBusiness-level activity only

Route Prefix

All Platform Admin endpoints live under /api/platform/. There is no tenant context in these routes β€” the Platform Admin sees the system holistically.

  • GET /api/platform/dashboard β€” KPI summary
  • GET /api/platform/tenants β€” list all tenants
  • POST /api/platform/tenants β€” create tenant
  • PUT/PATCH /api/platform/tenants/{id} β€” update tenant
  • DELETE /api/platform/tenants/{id} β€” soft-deactivate tenant
  • GET /api/platform/subscription-plans β€” list plans
  • POST /api/platform/subscription-plans β€” create plan
  • PUT/PATCH /api/platform/subscription-plans/{id} β€” update plan
  • DELETE /api/platform/subscription-plans/{id} β€” delete/deactivate plan
  • GET /api/platform/notifications β€” audit events
  • POST /api/platform/notifications/{id}/mark-read β€” mark one read
  • POST /api/platform/notifications/mark-all-read β€” bulk mark read
  • DELETE /api/platform/notifications/{id} β€” delete audit event
πŸ”

Authentication & Permission Model

PLT-002

Platform Admin authentication uses the same POST /api/login endpoint as other portals but requires user_type = 'platform_admin'. The rejectUnlessSuperAdmin() middleware guard is stricter than the standard portal check β€” it specifically validates that the authenticated user is a platform admin rather than any portal-type user.

Authentication Flow

1
PLATFORM ADMIN
Sends POST /api/login with email, password, user_type: "platform_admin"
2
SYSTEM
Validates credentials. Confirms user record has user_type = 'platform_admin'. Returns Bearer token.
3
PLATFORM ADMIN
Includes Authorization: Bearer {token} in every subsequent request to /api/platform/*
4
SYSTEM
rejectUnlessSuperAdmin() middleware checks token user type on each request. Returns 403 if check fails β€” even if token is valid for a different user_type.
Guard is not role-based fallback: A valid admin portal token will still fail the rejectUnlessSuperAdmin() check with HTTP 403. Platform Admin tokens cannot be used on regular tenant portal routes and vice versa.

Permission System

Beyond the super admin guard, each capability requires a granular permission assigned to the platform admin user:

Permission KeyScope & AllowsEndpoints Protected
tenants.view List and view all tenant (Business) records GET /api/platform/tenants, GET /api/platform/tenants/{id}
tenants.manage Create, update, and soft-deactivate tenants; supersedes view POST, PATCH, DELETE /api/platform/tenants/*
subscription_plans.view List and view subscription plan records GET /api/platform/subscription-plans
subscription_plans.manage Full CRUD on subscription plans; supersedes view POST, PATCH, DELETE /api/platform/subscription-plans/*
platform_notifications.view Read audit event log; filter and search events GET /api/platform/notifications, GET /api/platform/notifications/{id}
platform_notifications.manage Mark events read (individual or bulk); hard delete events; supersedes view POST mark-read, POST mark-all-read, DELETE /api/platform/notifications/{id}
Permissions are cumulative: A .manage permission implies the corresponding .view permission. Both guards must pass: (1) rejectUnlessSuperAdmin() then (2) the specific permission check. Missing either returns 403.

Token Security Notes

  • Platform Admin Bearer tokens should be treated as high-privilege credentials β€” store in secure vault, not env vars or source code.
  • Tokens are scoped to a single user_type β€” cross-type use is rejected server-side.
  • All platform admin actions are audit-logged via PlatformAuditLogger β€” token compromise creates a forensic trail.
  • On token expiry, re-authenticate via POST /api/login β€” returns a fresh token.
πŸ“Š

Dashboard KPIs

PLT-003

The Platform Dashboard (GET /api/platform/dashboard) returns a real-time aggregate view of the entire multi-tenant system. All counts are computed on-demand from the database β€” there is no caching layer between the API response and the underlying records.

KPI Fields & Derivations

// Platform Dashboard β€” KPI derivations (PlatformDashboardController::index) tenant_count = Business.count() // All Business records regardless of status active_tenant_count = Business WHERE is_active = true β†’ count() // Only tenants not suspended/deactivated suspended_tenant_count = tenant_count - active_tenant_count // Derived β€” not a direct DB query subscription_plan_count = SubscriptionPlan WHERE is_active = true β†’ count() // Only active plans (inactive/deactivated excluded) plan_breakdown = SubscriptionPlan.withCount('businesses as tenant_count') // Per-plan tenant count, ORDER BY monthly_price ASC recent_tenants = Business.with('subscriptionPlan').orderByDesc('created_at').limit(5) // 5 most recently onboarded tenants with their plan details

KPI Field Reference

Response FieldTypeSourceNotes
tenant_countintBusiness.count()Total tenants β€” all statuses
active_tenant_countintBusiness WHERE is_active=trueTenants currently operating
suspended_tenant_countinttenant_count - active_tenant_countArithmetic derivation
subscription_plan_countintSubscriptionPlan WHERE is_active=trueExcludes deactivated plans
plan_breakdownarraySubscriptionPlan.withCount(businesses)Ordered cheapest β†’ most expensive
recent_tenantsarray[5]Business.with(plan).limit(5)Latest 5 onboarded, includes plan name

plan_breakdown Object

Each entry in plan_breakdown represents one subscription plan with its assigned tenant count:

FieldTypeDescription
idintPlan ID
namestringPlan name (e.g. "Professional")
slugstringURL-safe identifier
monthly_pricenumericPlan price β€” sort key
tenant_countintNumber of Business records on this plan
is_activeboolOnly active plans in breakdown

Example Dashboard Snapshot

Example β€” Platform Dashboard Response (Production Snapshot)
tenant_count12
active_tenant_count10
suspended_tenant_count2 (= 12 - 10)
subscription_plan_count3 (Starter, Professional, Enterprise)
plan_breakdown[0].nameStarter Β· PKR 5,000/mo Β· 4 tenants
plan_breakdown[1].nameProfessional Β· PKR 15,000/mo Β· 6 tenants
plan_breakdown[2].nameEnterprise Β· PKR 35,000/mo Β· 2 tenants
recent_tenants[0]Al-Noor Engineering Β· joined 3 days ago
recent_tenants[1]Metro Civil Works Β· joined 1 week ago
recent_tenants[2–4]... 3 more tenants joined this month
Dashboard access is guard-only: PlatformDashboardController::index() calls rejectUnlessSuperAdmin() but does not require a specific permission beyond being a platform admin. The dashboard is read-only β€” it never modifies data.
🏒

Tenant Management

PLT-004

Tenant management is the primary responsibility of the Platform Admin. A "tenant" is a Business model record β€” each representing one engineering company subscribed to the platform. Required permission: tenants.manage (plus rejectUnlessSuperAdmin()).

Tenant Fields

FieldTypeSourceDescription
idintAUTOPrimary key
business_namestringREQUIREDFull business name (e.g. "Al-Noor Engineering")
owner_namestringREQUIREDPrimary contact person name
contact_emailstringREQUIREDPrimary contact email
contact_phonestringOPTIONALPhone number
subdomain_slugstringAUTOURL-safe slug derived from business_name; unique
subscription_plan_idintREQUIREDFK β†’ SubscriptionPlan (must be is_active=true)
subscription_statusenumAUTOactive | suspended | past_due | cancelled
is_activeboolAUTOtrue on creation; false = deactivated
created_attimestampAUTOOnboarding date

Create Tenant β€” Transaction Flow (CRITICAL)

Tenant creation runs inside a database transaction (DB::transaction). All steps succeed or the entire creation is rolled back. This ensures the Business record and its admin user are always created together.

1
PLATFORM ADMIN
Sends POST /api/platform/tenants with:
business_name REQUIRED  owner_name REQUIRED  contact_email REQUIRED  subscription_plan_id REQUIRED  contact_phone OPTIONAL  create_admin_user BOOL, default: true
2
SYSTEM
Validates all required fields. Checks subscription_plan_id: plan must exist AND is_active = true. If plan is inactive or missing β†’ 422 "Invalid subscription plan".
3
SYSTEM
Generates subdomain_slug:
base = Str::slug(business_name) (e.g. "Al-Noor Engineering" β†’ al-noor-engineering)
If slug already exists: appends -1, -2, ... until unique.
4
SYSTEM
BEGIN DB::transaction β€” Creates Business record with is_active=true, subscription_status='active', and the generated subdomain_slug.
5
SYSTEM
If create_admin_user = true: Creates User record with user_type='admin', business_id set to new Business ID, username = {subdomainSlug_admin} (dashes stripped + "_admin"), temp_password = random(12 chars).
6
SYSTEM
Calls PlatformAuditLogger.tenantCreated() β€” creates PlatformAuditEvent with event_code='TENANT_CREATED', actor = authenticated platform admin user. END transaction.
7
SYSTEM
Returns HTTP 201 with full Business data plus admin_invite object: { user_id, username, email, temporary_password }
8
PLATFORM ADMIN
Securely delivers username and temporary_password to the business owner (typically via phone or secure message β€” never via unsecured email).

Subdomain Slug & Admin Username Derivation

// Slug generation logic (TenantAdminController) base_slug = Str::slug(business_name) // Converts to lowercase, replaces spaces/special chars with hyphens // "Al-Noor Engineering" β†’ "al-noor-engineering" // "City Build Co." β†’ "city-build-co" // Uniqueness check loop: candidate = base_slug counter = 1 while Business.where(subdomain_slug = candidate).exists(): candidate = base_slug + "-" + counter++ subdomain_slug = candidate // final unique slug stored // Admin username: remove hyphens from slug, append "_admin" admin_username = str_replace('-', '', base_slug) + '_admin' // "al-noor-engineering" β†’ "alnoorengineering_admin" // "city-build-co" β†’ "citybuildco_admin"
Slug Examples
Input business_namesubdomain_slug β†’ admin_username
Al-Noor Engineeringal-noor-engineering β†’ alnoorengineering_admin
City Build Co.city-build-co β†’ citybuildco_admin
Apex Contractors (2nd)apex-contractors-1 β†’ apexcontractors_admin
Metro Civil Worksmetro-civil-works β†’ metroworks_admin

Tenant Status Lifecycle

Tenants have two independent status indicators that work together:

FieldValuesMeaning
is_activetrue / falseBinary active/deactivated flag. Changing to false triggers tenantSuspended() audit event.
subscription_statusactive | suspended | past_due | cancelledDetailed subscription state. Can be patched independently.

Subscription Status State Machine

active
⇄
suspended
β†’
past_due
β†’
cancelled
active
β†’
cancelled
is_active change triggers audit: When is_active transitions from true β†’ false (or false β†’ true) during an update, PlatformAuditLogger.tenantSuspended() is called automatically. This is in addition to the standard tenantUpdated() diff log.

Update Tenant (PATCH semantics)

PATCH /api/platform/tenants/{id} β€” all fields optional. Only supplied fields are updated. Changed fields are diffed and logged to the platform audit log.

FieldTagValidationAudit Notes
business_nameOPTIONALstringLogged in diff
owner_nameOPTIONALstringLogged in diff
contact_emailOPTIONALemail formatLogged in diff
contact_phoneOPTIONALstringLogged in diff
subscription_plan_idOPTIONALmust exist + is_active=truePlan change logged
subscription_statusOPTIONALin: active, suspended, past_due, cancelledStatus change logged
is_activeOPTIONALbooleanChange fires tenantSuspended() event

Destroy Tenant β€” Soft Deactivation

DELETE does NOT hard delete: DELETE /api/platform/tenants/{id} performs a soft deactivation. It sets is_active = false and subscription_status = 'suspended' then calls PlatformAuditLogger.tenantSuspended(). The Business record is preserved. Returns { id, is_active: false }.

Tenant Listing Filters

Query ParamTypeEffect
subscription_plan_idintFilter tenants by their assigned plan
is_activeboolean (0/1/true/false)Filter active or deactivated tenants
subscription_statusstringFilter by status (active/suspended/past_due/cancelled)

8-Step Tenant Onboarding Journey

1
PLATFORM ADMIN
Logs in to platform admin console β†’ views dashboard: tenant_count, active plans, plan_breakdown distribution.
2
PLATFORM ADMIN
Creates or verifies a subscription plan exists with is_active=true that matches the incoming tenant's needs (see PLT-005).
3
PLATFORM ADMIN
Calls POST /api/platform/tenants with business_name, owner_name, contact_email, subscription_plan_id, and create_admin_user: true.
4
SYSTEM
Validates plan is active. Generates subdomain_slug from business_name. Creates Business record (is_active=true, subscription_status='active').
5
SYSTEM
Creates admin User: user_type='admin', business_id = new tenant ID, username = {slug}_admin, generates secure 12-char temp_password.
6
SYSTEM
Fires PlatformAuditLogger.tenantCreated(). API response includes admin_invite: { user_id, username, email, temporary_password }.
7
PLATFORM ADMIN
Securely communicates username and temporary_password to the business owner via phone or secure channel.
8
BUSINESS ADMIN
Logs into admin portal using temporary credentials β†’ is prompted to change password β†’ begins setting up locations, trade types, staff, subcontractors, and first project.
πŸ“¦

Subscription Plans

PLT-005

Subscription plans define the feature limits and pricing for each tier of service offered on the platform. Required permission: subscription_plans.manage. All plan changes are logged via PlatformAuditLogger.

Plan Field Reference

FieldTypeTagValidationNotes
namestringREQUIREDmax:128Human-readable plan name
slugstringAUTOunique across all plansAuto-generated from name if not provided
monthly_pricenumericREQUIREDmin:0Can be 0 for free/trial plans
max_projectsintREQUIREDmin:-1-1 = unlimited
max_locationsintREQUIREDmin:-1-1 = unlimited
max_employeesintREQUIREDmin:-1-1 = unlimited
has_client_portalboolOPTIONALbooleanUnlocks client portal feature for tenants on this plan
is_activeboolOPTIONALbooleanInactive plans cannot be assigned to tenants

The -1 Unlimited Convention

-1 means unlimited: For max_projects, max_locations, and max_employees, the value -1 signals no limit. The API normalizes this value server-side β€” client code should send -1 literally to indicate unlimited. Any non-negative integer sets a hard cap enforced within the tenant portal.

Plan Slug Auto-Generation

// Plan slug generation (SubscriptionPlanAdminController) base_slug = Str::slug(name) // "Professional" β†’ "professional" // "Enterprise Plus" β†’ "enterprise-plus" // Uniqueness check β€” same pattern as tenant slugs: while SubscriptionPlan.where(slug = candidate).exists(): candidate = base_slug + "-" + counter++

Example Plans

PlanMonthly Pricemax_projectsmax_locationsmax_employeeshas_client_portal
Starter PKR 5,000/mo 5 2 10 No
Professional PKR 15,000/mo 25 10 50 Yes
Enterprise PKR 35,000/mo -1 (unlimited) -1 (unlimited) -1 (unlimited) Yes
Trial PKR 0/mo 2 1 5 No

has_client_portal Flag

When has_client_portal = false, tenants on this plan cannot activate or use the client portal feature β€” even if they create client users. The tenant admin portal may show the feature as locked or unavailable. Upgrading a tenant's plan to one with has_client_portal = true immediately unlocks the feature without data loss.

Plan Lifecycle & Destruction Rules

is_active: true
β†’ assigned to tenants
tenants using plan
β†’ try DELETE
deactivated (is_active=false)
422 returned
is_active: true/false
β†’ 0 tenants assigned
no tenants
β†’ DELETE
hard deleted
Deletion guard: DELETE /api/platform/subscription-plans/{id} checks Business WHERE subscription_plan_id = $plan→id. If count > 0, the plan is deactivated (not deleted) and the API returns 422 "Plan has assigned tenants; deactivated instead of deleted". The response includes the now-deactivated plan. To truly delete a plan, first reassign all tenants to a different plan.

5-Step Plan Creation Flow

1
PLATFORM ADMIN
Sends POST /api/platform/subscription-plans with name, monthly_price, max_projects, max_locations, max_employees, has_client_portal.
2
SYSTEM
Validates required fields. Checks all max_* values are integer β‰₯ -1. Confirms monthly_price β‰₯ 0.
3
SYSTEM
Auto-generates slug from name (unless explicitly provided). Ensures uniqueness with suffix if collision.
4
SYSTEM
Creates SubscriptionPlan record with is_active=true. Calls PlatformAuditLogger.planCreated() β†’ PLAN_CREATED audit event.
5
PLATFORM ADMIN
Receives 201 response with full plan object including generated slug and id. Plan is immediately available for tenant assignment.

Plan Validation Quick Reference

RuleDetail
Plan must be active to assignTenant creation/update with an inactive plan_id β†’ 422 "Invalid subscription plan"
-1 is the only unlimited markerAny integer β‰₯ 0 sets a hard cap; -1 = no limit
Slug must be globally uniqueManual slug on update that conflicts β†’ 422 "Slug already in use"
monthly_price can be 0Free/trial plans allowed (min:0, not min:1)
name max 128 charsLonger names will fail validation
πŸ“‹

Notifications & Audit Logs

PLT-006

The Platform Audit Log is a table of PlatformAuditEvent records that capture every significant operation performed through the Platform Admin console. Events are auto-created by PlatformAuditLogger and are never sent to tenant users β€” this is the platform operator's internal forensic trail.

Required guard: rejectUnlessSuperAdmin(). Required permission: platform_notifications.view (or platform_notifications.manage for write operations).

Internal only β€” not visible to tenants: PlatformAuditEvent records are not exposed to any tenant portal (Admin, Employee, Client, Contractor). Tenants cannot see platform-level audit events. This log is exclusively for the SaaS operator's oversight.

Two separate API concerns

PurposeControllerEndpointsNotes
Bell dropdown PlatformNotificationController GET /notifications
PATCH /notifications/read-all
Max 20 most recent unread only. No pagination key.
Audit log screen PlatformAuditLogController GET /audit-logs + full CRUD (5 endpoints) Paginated. Supports all filters. Full history.

Audit Event Fields

FieldTypeDescription
idintUnique event ID β€” searchable
titlestringShort human-readable event title β€” searchable
bodytextDetailed event description, often includes diff data β€” searchable
event_codestringMachine-readable code (e.g. TENANT_CREATED) β€” searchable, filterable
categorystringGrouping category (e.g. "tenant", "plan") β€” filterable
severitystringe.g. "info", "warning", "action_taken" β€” filterable
entity_typestringe.g. "business", "subscription_plan" β€” filterable
is_readboolRead status β€” filterable (0/1/true/false)
actorUserEager-loaded user who triggered the event; null for system events
created_attimestampWhen the event occurred

Event Codes

TENANT_CREATED
Fired by PlatformAuditLogger.tenantCreated() when a new Business is onboarded via POST /api/platform/tenants. Actor = platform admin who created the tenant.
TENANT_UPDATED
Fired by PlatformAuditLogger.tenantUpdated() after any field changes on a Business record. Body contains the diff of changed fields.
TENANT_SUSPENDED
Fired by PlatformAuditLogger.tenantSuspended() when is_active changes (in update) OR when DELETE /api/platform/tenants/{id} is called.
PLAN_CREATED
Fired by PlatformAuditLogger.planCreated() when a new SubscriptionPlan is created.
PLAN_UPDATED
Fired by PlatformAuditLogger.planUpdated() when plan limits or pricing are changed. Body includes price/limit diff.

Audit Log Filters (GET /api/platform/audit-logs)

Query ParamTypeEffect
categorystringFilter by category (e.g. "tenant", "plan")
severitystringFilter by severity (e.g. "info", "warning")
entity_typestringFilter by entity type (e.g. "business", "subscription_plan")
is_read0/1/true/falseFilter unread (0) or read (1) events
searchstringFull-text search across title, body, event_code, category, and id
per_pageintDefault 15; paginated response

Audit Log Operations

OperationEndpointPermissionNotes
Bell dropdown (recent unread)GET /api/platform/notificationsviewMax 20 unread, newest first, no pagination
Mark all read (bell)PATCH /api/platform/notifications/read-allmanageClears bell badge
List all eventsGET /api/platform/audit-logsviewPaginated, all filters supported
View single eventGET /api/platform/audit-logs/{id}viewReturns event with actor loaded
Mark all readPATCH /api/platform/audit-logs/read-allmanageBulk: all unread β†’ read
Mark one readPATCH /api/platform/audit-logs/{id}/readmanageSets is_read = true on one event
Delete eventDELETE /api/platform/audit-logs/{id}manageHard delete β€” permanent, no soft delete
Hard delete is permanent: Deleting a PlatformAuditEvent is a hard delete β€” there is no soft delete or recycle bin. Only delete audit events if you have an external audit trail backup or compliance policy that permits it. Typically, only mark events as read rather than deleting them.
πŸš€

End-to-End Tenant Onboarding Scenario

PLT-007

This 15-step scenario walks through the complete lifecycle of onboarding a new engineering business β€” "Al-Noor Engineering" β€” from plan creation to the tenant's first active project, as seen from the Platform Admin perspective.

1
PLATFORM ADMIN  Logs into platform admin console. Dashboard shows: tenant_count: 10, active_tenant_count: 10, subscription_plan_count: 2 (Starter + Enterprise only), recent_tenants shows last 5 onboarded businesses.
2
PLATFORM ADMIN  Creates "Professional" subscription plan:
POST /api/platform/subscription-plans
name: "Professional" Β· monthly_price: 15000 Β· max_projects: 25 Β· max_locations: 10 Β· max_employees: 50 Β· has_client_portal: true
3
SYSTEM  Plan created successfully. Auto-generated slug: "professional". PlatformAuditLogger.planCreated() fires β†’ audit event PLAN_CREATED logged with actor = Platform Admin. Dashboard subscription_plan_count now shows 3.
4
PLATFORM ADMIN  Onboards Al-Noor Engineering:
POST /api/platform/tenants
business_name: "Al-Noor Engineering"
owner_name: "Mr. Bilal Ahmed"
contact_email: "bilal@alnoor.pk"
subscription_plan_id: 3 (Professional)
create_admin_user: true
5
SYSTEM  Validates plan ID=3 is active. Generates subdomain_slug = "al-noor-engineering" via Str::slug("Al-Noor Engineering"). Checks no existing Business uses this slug β†’ unique, no suffix needed.
6
SYSTEM  Derives admin username: str_replace('-','','al-noor-engineering') + '_admin' = "alnoorengineering_admin"
7
SYSTEM  Generates secure temporary password: "Kx7@mPqZ3nWr" (random 12 chars, mixed case, digits, symbols).
8
SYSTEM  DB::transaction executes: Creates Business (id=11, is_active=true, status='active') β†’ Creates User (user_type='admin', business_id=11, username='alnoorengineering_admin', hashed temp_password).
9
SYSTEM  PlatformAuditLogger.tenantCreated() fires β†’ TENANT_CREATED event logged. API returns HTTP 201 with:
admin_invite: { user_id: 47, username: "alnoorengineering_admin", email: "bilal@alnoor.pk", temporary_password: "Kx7@mPqZ3nWr" }
10
PLATFORM ADMIN  Calls Mr. Bilal Ahmed on phone. Verbally communicates: username alnoorengineering_admin and temporary password Kx7@mPqZ3nWr. Does NOT send via unsecured email.
11
BUSINESS ADMIN  Mr. Bilal logs into the admin portal using temporary credentials. System prompts password change. Mr. Bilal sets a strong permanent password and acknowledges terms of service.
12
BUSINESS ADMIN  Mr. Bilal sets up the Al-Noor Engineering tenant: creates 2 office locations, configures trade types (civil, structural), adds 8 staff members, links 3 subcontractors.
13
BUSINESS ADMIN  Mr. Bilal creates first project "Lahore Bridge Repair β€” Phase 1". Invites client user (client portal enabled because plan has has_client_portal: true). Client portal goes live.
14
PLATFORM ADMIN  One month later: Checks platform dashboard β†’ tenant_count: 11, active_tenant_count: 11. Al-Noor Engineering visible in plan_breakdown under Professional (now 7 tenants on Professional).
15
PLATFORM ADMIN  Reviews audit log β†’ GET /api/platform/notifications?search=al-noor. Sees: TENANT_CREATED (onboarding day) + TENANT_UPDATED (password change event) + PLAN_CREATED (Professional plan creation). All events show correct actor and timestamps.
Key outcome: The entire tenant onboarding from Platform Admin request to Business Admin first login takes under 5 minutes. The platform audit log provides complete traceability from plan creation through tenant activation.
⚠️

Error Reference

PLT-008

All Platform Admin endpoints follow consistent HTTP error semantics. Errors include a message field describing the issue. Validation errors (422) include a errors object with per-field details.

HTTP Code Error Message / Condition Root Cause Resolution
401 Unauthorized Token missing, malformed, or expired No Authorization header, or Bearer token has expired Re-authenticate via POST /api/login with platform_admin credentials; use fresh token
403 Forbidden rejectUnlessSuperAdmin() check failed Token belongs to a non-platform_admin user_type (e.g. admin portal token used on platform route) Authenticate as a user with user_type = 'platform_admin' specifically
403 Forbidden Missing permission (e.g. tenants.manage) Platform admin user lacks the required granular permission for the operation Grant the required permission (tenants.manage, subscription_plans.manage, etc.) to the platform admin user
404 Not Found Tenant or plan ID not found The {id} in the route does not correspond to any Business or SubscriptionPlan record Verify the ID via the list endpoint; ensure the record was not deleted
422 "Invalid subscription plan" subscription_plan_id references a plan that does not exist or has is_active = false Use a plan ID where is_active = true. Check plan status via GET /api/platform/subscription-plans
422 "Plan has assigned tenants; deactivated instead of deleted" Attempted DELETE /api/platform/subscription-plans/{id} on a plan with 1+ active tenant assignments Reassign all tenants from this plan to another plan first, then retry delete. Or accept the deactivation (plan is now is_active=false)
422 "Slug already in use" Attempted to set a plan slug to a value already used by another plan Choose a different slug or omit it (allow auto-generation with uniqueness suffix)
422 Validation errors (various) Required fields missing, invalid types, out-of-range values (e.g. max_projects < -1) Check errors object in response for per-field details. Ensure all max_* values β‰₯ -1, monthly_price β‰₯ 0, name ≀ 128 chars

Common Mistakes

  • Using an admin portal token on /api/platform/* routes β€” always re-auth as platform_admin
  • Assigning a deactivated plan during tenant create/update β€” check is_active before assignment
  • Expecting hard delete on tenants β€” DELETE only soft-deactivates; use PATCH is_active:false for the same effect
  • Confusing subscription_status with is_active β€” both exist independently
  • Expecting DELETE on plans with tenants to work β€” always deactivates instead, returns 422

Debug Checklist

1
CHECK
Is the token from a platform_admin user? Check user_type field of the authenticated user.
2
CHECK
Does the platform_admin user have the required permission? (tenants.manage, subscription_plans.manage, platform_notifications.manage)
3
CHECK
For tenant create/update: is the subscription_plan_id referencing an active plan? (is_active = true)
4
CHECK
For plan delete: are there tenants on this plan? (GET /api/platform/tenants?subscription_plan_id={id})
5
CHECK
Check platform audit log for recent events β€” GET /api/platform/notifications?is_read=0 shows unread events triggered by the failed operation.
All errors are audit-safe: Failed operations that pass the auth guard still create PlatformAuditEvent entries where applicable. A failed tenant creation (e.g. invalid plan) does not create a TENANT_CREATED event β€” the transaction rolls back cleanly. But successful operations followed by manual reversal will show both events in the log.

Engineering Services API Β· Platform Admin Business Guide Β· ← Documentation Hub