Dashboard & Analytics
Role-aware dashboards for SaaS Admin, Client Admin, Gym Manager and Staff. KPI cards, revenue trends, attendance stats and quick actions.
A production-grade, multi-tenant gym management platform built with Laravel, Blade and Tailwind CSS. Single database, logical tenant isolation, light & dark mode.
GymSync is a SaaS platform where a platform owner (SaaS Admin) onboards client organizations and provisions gyms/branches for them. Each client pays a monthly subscription, and each gym runs day-to-day operations: members, memberships, payments, attendance, staff, inventory and reporting.
Every tenant record carries client_id; gym operations also carry gym_id. Context is always derived from the authenticated user, never trusted from the request.
Policies, role middleware, Form Requests and tenant middleware combine to enforce access at every layer.
Pure Blade + Tailwind with Alpine.js only for small interactions. No SPA, no heavy UI frameworks.
Core technologies and how they are used.
| Layer | Technology | Purpose |
|---|---|---|
| Backend | PHP 8.5 / Laravel 13 | Routing, Eloquent ORM, queues, scheduler, mail, validation |
| Database | MySQL | Single shared schema, logical multi-tenancy, soft deletes |
| Frontend | Blade + Tailwind CSS | Server-rendered views, responsive components, theming tokens |
| Interactivity | Vanilla JS / Alpine.js | Sidebar, dropdowns, modals, theme toggle, small fetch calls |
| Auth | Laravel Breeze-style + Spatie Permissions | Session auth, roles, email verification, password reset |
| DomPDF | Receipts, invoices, salary slips, reports | |
| Testing | PHPUnit | 447 feature/unit tests, RefreshDatabase, SQLite in-memory |
Seven roles span the platform owner, organization owners and gym staff.
Platform owner. Creates clients & gyms, manages plans, subscriptions, invoices, payments, reports, activity logs and support queries. Global bypass for tenant scoping.
Owns an organization. Manages all branches, staff, trainers, members, plans, finances, inventory, reports, settings and support queries. Cannot create new gyms.
Operates assigned branch(es): members, memberships, attendance, equipment, products, leaves and support queries for that gym.
Front-desk operations: member check-in, memberships, payments and receipts scoped to the active gym.
Financial modules: payments, expenses, salaries and financial reports. Can force-delete financial records.
Additional on-site roles for managing reception flow and gate/security checks at check-in.
The platform enforces a strict hierarchy so one tenant can never see another's data.
Every tenant-owned table stores the owning client. Derived from the authenticated user's context.
Gym-operational tables also store the branch. The TenantContext service resolves the active gym.
IDs are never taken from hidden fields or query strings. Policies + middleware validate access on every request.
Every major capability of the platform, with representative route names.
Role-aware dashboards for SaaS Admin, Client Admin, Gym Manager and Staff. KPI cards, revenue trends, attendance stats and quick actions.
SaaS Admin creates client organizations, approves pending sign-ups, manages status (pending/active/suspended/inactive) and views subscription details.
Create gyms/branches with branch codes, timezone, currency. Branch switching for multi-branch clients, status control and impersonation to support a gym.
Plans define monthly price, max gyms/users/members and extra-gym pricing. Subscriptions track base + extra gym amounts, statuses and billing cycle.
Monthly platform billing with partial payments, tax (GST/FED), discounts, adjustments, refunds and voiding. Multiple payments per invoice are supported.
Full member lifecycle: registration, profile, PIN & device UID, assigned trainer, status control (active/inactive/blocked) and soft-delete/restore.
Configurable plans by category (male/female/student/family/custom), duration units (day/week/month/year) and fee with active/inactive status.
Assign plans, renew, change plan, freeze/resume and track price, discount, payable, paid and balance. Only one active subscription per member is enforced.
Record member payments (cash, bank, jazzcash, easypaisa, card, other), auto-generate PDF receipts, email receipts to members and owners.
PIN, manual and fingerprint check-in/out. Open-session protection, expired-member blocking, biometric device registry and an attendance simulator for testing.
Lead pipeline with statuses (new/contacted/trial/converted/lost), call logging, trial dates, assignment to trainers and one-click conversion to member.
Trainer profiles with specialization, rates, bank details, commission and status. Members can be assigned to trainers.
Staff user management with roles, salary calculation (base + bonus + commission − deductions), salary slip emailing and leave types/approvals.
Product categories, products with stock and low-stock thresholds, stock movements and POS-style sales that generate invoices for members or customers.
Equipment categories and asset records with cost, purchase date, condition, status and maintenance tracking.
Operational expenses by category with recurrence, payment method and approval status.
Payment, attendance, subscription-expiry and member reports plus platform-level reports and monthly summary generation with exportable output.
Collect feedback via a shareable public link or manually. Star ratings, sources (member/lead/public/manual), status workflow and request-by-email flow.
Read-only natural-language assistant scoped to the active gym. Answers attendance, renewals, collections and member-lookup questions with safe, parameterised queries.
Client Admin / Gym Manager raise queries, problems, issues, complaints or suggestions from the active branch. Threaded conversation with the SaaS Admin, status workflow (new/open/in_progress/resolved/closed) and email notifications both ways.
Email/SMS/WhatsApp notification log with per-member and per-user preferences, receipt, expiry and owner-payment notification toggles.
Immutable audit trail of user actions, logins/logouts and failed attempts with user, role, entity and IP metadata. Scoped by client/gym for tenant safety.
Per-gym branding (logo, favicon), contact details, custom SMTP, receipt/invoice templates with theme colours, commission percentages and support email configuration.
Soft-deleted records across modules with restore and role-gated force-delete to permanently purge data.
A two-way support desk connecting Client Admins / Gym Managers with the SaaS Admin.
See queries from all branches and clients with the branch name displayed. Reply, update status (new, open, in_progress, resolved, closed) and record resolution notes.
When a new query is submitted, an email is sent to the SaaS Admin support address configured per gym (saas_admin_support_email), falling back to the mail-from address. Subject, type, branch and message are included with a link to open the query.
When the SaaS Admin replies, the client's from_email receives a threaded reply email with a button to view the conversation.
| Setting | Column | Purpose |
|---|---|---|
| Gym Support Email | support_email | Email for gym-specific support queries |
| SaaS Admin Support Email | saas_admin_support_email | Destination for new query notifications |
| Enable support notifications | support_notifications_enabled | Master toggle for support notification emails |
39 Eloquent models grouped by domain area.
Core tables (39 shown). All tenant tables carry client_id; gym records also carry gym_id.
| Table | Description | Notable Columns |
|---|---|---|
clients | Client organizations / tenants | status, billing_cycle, next_billing_date |
users | All users across roles (saas_admin → client_admin → staff) | role, status, client_id |
gyms | Branches belonging to a client | branch_code, timezone, currency, status |
gym_user | User ↔ gym assignment pivot | role, status |
gym_settings | Per-gym configuration & branding | SMTP, receipts, support emails, commission |
gym_extra_charges | Extra-gym billing charges | amount, effective dates, status |
saas_plans | Subscription plan catalogue | price, max gyms/users/members |
saas_subscriptions | Client plan subscriptions | base + extra amounts, status |
saas_invoices | Platform invoices | totals, tax, discount, due date, status |
saas_payments | Platform payments (partial allowed) | amount, method, status |
members | Gym members | member_no, pin_hash, device_uid, status |
membership_plans | Plan definitions | category, duration, fee |
member_subscriptions | Member plan assignments | dates, amounts, balance, status |
member_subscription_freezes | Freeze periods | freeze dates, extended days |
member_payments | Member payments | amount, method, status |
receipts | Generated payment receipts | receipt_no, pdf_path, sent flags |
attendance_logs | Check-in / check-out sessions | method (pin/manual/fingerprint) |
biometric_devices | Registered attendance devices | device_id, api_token, last_seen |
leads | Sales / enquiry pipeline | status, source, trial_date |
lead_activities | Lead interaction history | type, subject, activity_at |
trainers | Trainer records incl. bank details | specialization, rates, status |
staff_salaries | Payroll records | base, bonus, commission, net, status |
leaves | Leave requests | type, dates, days, status |
leave_types | Leave type catalogue | is_paid, deduction, max days |
products | Retail products / stock | sku, prices, stock qty |
product_categories | Product categorisation | parent_id, slug |
product_stock_movements | Stock ledger | type, qty, stock_after |
sales | Retail sales / POS | invoice_no, totals, status |
sale_items | Sale line items | product, qty, line total |
equipment | Gym equipment assets | condition, status, purchase info |
equipment_categories | Equipment categorisation | slug, status |
expenses | Operational expenses | category, amount, status |
feedback | Feedback / ratings | rating, source, status |
feedback_requests | Feedback request emails | token, status, sent_at |
notifications | Notification delivery log | type, channel, status |
notification_preferences | Per member/user prefs | channels, toggles |
activity_logs | Audit trail | action, user, entity, ip |
support_conversations | Support queries (NEW) | subject, type, status, branch, resolved_by |
support_messages | Support conversation messages (NEW) | sender_type, message, attachments |
All 246 application routes grouped by access area.
| Method | URI | Name |
|---|---|---|
| GET | /activity-logs | client.activity-logs.index |
| GET | /activity-logs/{activityLog} | client.activity-logs.show |
| POST | /assistant | client.assistant.ask |
| GET | /assistant | client.assistant.index |
| GET | /attendance | client.attendance.index |
| POST | /attendance/manual | client.attendance.manual |
| POST | /attendance/manual/staff | client.attendance.manual.staff |
| POST | /attendance/toggle | client.attendance.toggle |
| GET | /biometric-devices/create | client.biometric-devices.create |
| DELETE | /biometric-devices/{biometricDevice} | client.biometric-devices.destroy |
| GET | /biometric-devices/{biometricDevice}/edit | client.biometric-devices.edit |
| GET | /biometric-devices | client.biometric-devices.index |
| POST | /biometric-devices | client.biometric-devices.store |
| PUT | /biometric-devices/{biometricDevice} | client.biometric-devices.update |
| GET | /attendance/simulator | client.biometric-simulator.index |
| POST | /attendance/simulator/scan | client.biometric-simulator.scan |
| GET | /branches | client.branches.index |
| POST | /branches/{gym}/switch | client.branches.switch |
| GET | /client/dashboard | client.dashboard |
| GET | /equipment-categories/create | client.equipment-categories.create |
| DELETE | /equipment-categories/{category} | client.equipment-categories.destroy |
| GET | /equipment-categories/{category}/edit | client.equipment-categories.edit |
| GET | /equipment-categories | client.equipment-categories.index |
| POST | /equipment-categories | client.equipment-categories.store |
| PUT | /equipment-categories/{category} | client.equipment-categories.update |
| GET | /equipment/create | client.equipment.create |
| DELETE | /equipment/{equipment} | client.equipment.destroy |
| GET | /equipment/{equipment}/edit | client.equipment.edit |
| GET | /equipment | client.equipment.index |
| POST | /equipment | client.equipment.store |
| PUT | /equipment/{equipment} | client.equipment.update |
| GET | /expenses/create | client.expenses.create |
| DELETE | /expenses/{expense} | client.expenses.destroy |
| GET | /expenses/{expense}/edit | client.expenses.edit |
| GET | /expenses | client.expenses.index |
| PATCH | /expenses/{expense}/status | client.expenses.status |
| POST | /expenses | client.expenses.store |
| PUT | /expenses/{expense} | client.expenses.update |
| GET | /feedback/create | client.feedback.create |
| DELETE | /feedback/{feedback} | client.feedback.destroy |
| GET | /feedback | client.feedback.index |
| POST | /feedback/public-link | client.feedback.public-link |
| GET | /feedback/requests | client.feedback.requests.create |
| DELETE | /feedback/requests/{feedbackRequest} | client.feedback.requests.destroy |
| POST | /feedback/requests | client.feedback.requests.store |
| GET | /feedback/{feedback} | client.feedback.show |
| PATCH | /feedback/{feedback}/status | client.feedback.status |
| POST | /feedback | client.feedback.store |
| POST | /leads/{lead}/convert | client.leads.convert |
| GET | /leads/create | client.leads.create |
| DELETE | /leads/{lead} | client.leads.destroy |
| GET | /leads/{lead}/edit | client.leads.edit |
| GET | /leads | client.leads.index |
| GET | /leads/{lead} | client.leads.show |
| POST | /leads | client.leads.store |
| PUT | /leads/{lead} | client.leads.update |
| GET | /leave-types/create | client.leave-types.create |
| DELETE | /leave-types/{leaveType} | client.leave-types.destroy |
| GET | /leave-types/{leaveType}/edit | client.leave-types.edit |
| GET | /leave-types | client.leave-types.index |
| POST | /leave-types | client.leave-types.store |
| PUT | /leave-types/{leaveType} | client.leave-types.update |
| POST | /leaves/{leave}/approve | client.leaves.approve |
| GET | /leaves/create | client.leaves.create |
| DELETE | /leaves/{leave} | client.leaves.destroy |
| GET | /leaves | client.leaves.index |
| POST | /leaves/{leave}/reject | client.leaves.reject |
| POST | /leaves | client.leaves.store |
| GET | /members/create | client.members.create |
| DELETE | /members/{member} | client.members.destroy |
| GET | /members/{member}/edit | client.members.edit |
| GET | /members | client.members.index |
| GET | /members/{member} | client.members.show |
| PATCH | /members/{member}/status | client.members.status |
| POST | /members | client.members.store |
| PUT | /members/{member} | client.members.update |
| GET | /membership-plans/create | client.membership-plans.create |
| DELETE | /membership-plans/{plan} | client.membership-plans.destroy |
| GET | /membership-plans/{plan}/edit | client.membership-plans.edit |
| GET | /membership-plans | client.membership-plans.index |
| POST | /membership-plans | client.membership-plans.store |
| PUT | /membership-plans/{plan} | client.membership-plans.update |
| GET | /payments/create | client.payments.create |
| GET | /payments | client.payments.index |
| GET | /payments/{payment} | client.payments.show |
| POST | /payments | client.payments.store |
| GET | /products/categories/create | client.product-categories.create |
| DELETE | /products/categories/{category} | client.product-categories.destroy |
| GET | /products/categories/{category}/edit | client.product-categories.edit |
| GET | /products/categories | client.product-categories.index |
| GET | /products/categories/{category} | client.product-categories.show |
| POST | /products/categories | client.product-categories.store |
| PUT | /products/categories/{category} | client.product-categories.update |
| GET | /products/create | client.products.create |
| DELETE | /products/{product} | client.products.destroy |
| GET | /products/{product}/edit | client.products.edit |
| GET | /products | client.products.index |
| GET | /products/{product} | client.products.show |
| POST | /products/{product}/stock | client.products.stock.store |
| POST | /products | client.products.store |
| PUT | /products/{product} | client.products.update |
| GET | /receipts/{receipt}/download | client.receipts.download |
| GET | /receipts | client.receipts.index |
| POST | /receipts/{receipt}/resend | client.receipts.resend |
| GET | /receipts/{receipt} | client.receipts.show |
| GET | /renewals | client.renewals.index |
| GET | /reports/attendance | client.reports.attendance |
| GET | /reports/expiring | client.reports.expiring |
| POST | /reports/expiring/{subscription}/remind | client.reports.expiring.remind |
| GET | /reports/generate | client.reports.generate |
| GET | /reports | client.reports.index |
| GET | /reports/leave-balances | client.reports.leave-balances |
| GET | /reports/members | client.reports.members |
| GET | /reports/payments | client.reports.payments |
| POST | /salaries/{salary}/cancel | client.salaries.cancel |
| GET | /salaries/create | client.salaries.create |
| DELETE | /salaries/{salary} | client.salaries.destroy |
| GET | /salaries/{salary}/edit | client.salaries.edit |
| GET | /salaries | client.salaries.index |
| POST | /salaries/{salary}/mark-paid | client.salaries.mark-paid |
| GET | /salaries/preview | client.salaries.preview |
| POST | /salaries | client.salaries.store |
| PUT | /salaries/{salary} | client.salaries.update |
| GET | /sales/create | client.sales.create |
| DELETE | /sales/{sale} | client.sales.destroy |
| GET | /sales | client.sales.index |
| GET | /sales/{sale}/invoice | client.sales.invoice |
| POST | /sales/{sale}/resend | client.sales.resend |
| GET | /sales/{sale} | client.sales.show |
| POST | /sales | client.sales.store |
| GET | /settings | client.settings.edit |
| GET | /settings/pdf-design-preview | client.settings.pdf-design-preview |
| GET | /settings/pdf-preview | client.settings.pdf-preview |
| PUT | /settings | client.settings.update |
| POST | /subscriptions/{subscription}/change-plan | client.subscriptions.change-plan |
| GET | /subscriptions/{subscription}/change-plan | client.subscriptions.change-plan.create |
| GET | /subscriptions/create | client.subscriptions.create |
| POST | /subscriptions/{subscription}/freeze | client.subscriptions.freeze |
| GET | /subscriptions/{subscription}/freeze | client.subscriptions.freeze.create |
| GET | /subscriptions | client.subscriptions.index |
| POST | /subscriptions/{subscription}/renew | client.subscriptions.renew |
| GET | /subscriptions/{subscription}/renew | client.subscriptions.renew.create |
| POST | /subscriptions/{subscription}/resume | client.subscriptions.resume |
| GET | /subscriptions/{subscription} | client.subscriptions.show |
| POST | /subscriptions | client.subscriptions.store |
| GET | /support/create | client.support.create |
| GET | /support | client.support.index |
| POST | /support/{support}/reply | client.support.reply |
| GET | /support/{support} | client.support.show |
| POST | /support | client.support.store |
| GET | /trainers/create | client.trainers.create |
| DELETE | /trainers/{trainer} | client.trainers.destroy |
| GET | /trainers/{trainer}/edit | client.trainers.edit |
| GET | /trainers | client.trainers.index |
| GET | /trainers/{trainer} | client.trainers.show |
| PATCH | /trainers/{trainer}/status | client.trainers.status |
| POST | /trainers | client.trainers.store |
| PUT | /trainers/{trainer} | client.trainers.update |
| DELETE | /trash/{module}/{id} | client.trash.force-delete |
| GET | /trash | client.trash.index |
| POST | /trash/{module}/{id}/restore | client.trash.restore |
| GET | /client/users/create | client.users.create |
| DELETE | /client/users/{user} | client.users.destroy |
| GET | /client/users/{user}/edit | client.users.edit |
| GET | /client/users | client.users.index |
| PATCH | /client/users/{user}/status | client.users.status |
| POST | /client/users | client.users.store |
| PUT | /client/users/{user} | client.users.update |
| GET | /gym/dashboard | gym.dashboard |
| GET | /gym/select | gym.select |
| POST | /gym/select | gym.select.store |
| POST | /impersonation/stop | impersonation.stop |
| Method | URI | Name |
|---|---|---|
| GET | /super-admin/activity-logs | saas.activity-logs.index |
| GET | /super-admin/activity-logs/{activityLog} | saas.activity-logs.show |
| POST | /super-admin/clients/{client}/approve | saas.clients.approve |
| GET | /super-admin/clients/create | saas.clients.create |
| DELETE | /super-admin/clients/{client} | saas.clients.destroy |
| GET | /super-admin/clients/{client}/edit | saas.clients.edit |
| GET | /super-admin/clients | saas.clients.index |
| GET | /super-admin/clients/{client} | saas.clients.show |
| POST | /super-admin/clients | saas.clients.store |
| PUT | /super-admin/clients/{client} | saas.clients.update |
| GET | /super-admin/dashboard | saas.dashboard |
| GET | /super-admin/gyms/create | saas.gyms.create |
| DELETE | /super-admin/gyms/{gym} | saas.gyms.destroy |
| GET | /super-admin/gyms/{gym}/edit | saas.gyms.edit |
| POST | /super-admin/gyms/{gym}/impersonate | saas.gyms.impersonate |
| GET | /super-admin/gyms | saas.gyms.index |
| GET | /super-admin/gyms/{gym} | saas.gyms.show |
| PATCH | /super-admin/gyms/{gym}/status | saas.gyms.status |
| POST | /super-admin/gyms | saas.gyms.store |
| PUT | /super-admin/gyms/{gym} | saas.gyms.update |
| PATCH | /super-admin/invoices/{invoice}/adjustments | saas.invoices.adjustments |
| GET | /super-admin/invoices | saas.invoices.index |
| POST | /super-admin/invoices/{invoice}/payments | saas.invoices.payments.store |
| GET | /super-admin/invoices/{invoice} | saas.invoices.show |
| PATCH | /super-admin/invoices/{invoice}/status | saas.invoices.status |
| GET | /super-admin/payments | saas.payments.index |
| POST | /super-admin/payments/{payment}/refund | saas.payments.refund |
| POST | /super-admin/payments/{payment}/void | saas.payments.void |
| GET | /super-admin/plans/create | saas.plans.create |
| DELETE | /super-admin/plans/{plan} | saas.plans.destroy |
| GET | /super-admin/plans/{plan}/edit | saas.plans.edit |
| GET | /super-admin/plans | saas.plans.index |
| POST | /super-admin/plans | saas.plans.store |
| PUT | /super-admin/plans/{plan} | saas.plans.update |
| GET | /super-admin/reports/generate | saas.reports.generate |
| GET | /super-admin/reports/history/{report}/download | saas.reports.history.download |
| GET | /super-admin/reports | saas.reports.index |
| GET | /super-admin/settings | saas.settings.index |
| GET | /super-admin/settings/{gym} | saas.settings.show |
| PUT | /super-admin/settings/{gym} | saas.settings.update |
| POST | /super-admin/subscriptions/{subscription}/cancel | saas.subscriptions.cancel |
| GET | /super-admin/subscriptions/create | saas.subscriptions.create |
| POST | /super-admin/subscriptions/{subscription}/downgrade | saas.subscriptions.downgrade |
| GET | /super-admin/subscriptions/{subscription}/edit | saas.subscriptions.edit |
| GET | /super-admin/subscriptions | saas.subscriptions.index |
| GET | /super-admin/subscriptions/{subscription} | saas.subscriptions.show |
| POST | /super-admin/subscriptions | saas.subscriptions.store |
| PUT | /super-admin/subscriptions/{subscription} | saas.subscriptions.update |
| POST | /super-admin/subscriptions/{subscription}/upgrade | saas.subscriptions.upgrade |
| GET | /super-admin/support | saas.support.index |
| POST | /super-admin/support/{support}/reply | saas.support.reply |
| GET | /super-admin/support/{support} | saas.support.show |
| PATCH | /super-admin/support/{support}/status | saas.support.status |
| Method | URI | Name |
|---|---|---|
| GET | // | — |
| POST | /confirm-password | — |
| GET | /login | login |
| POST | /login | — |
| POST | /logout | logout |
| GET | /confirm-password | password.confirm |
| POST | /forgot-password | password.email |
| GET | /forgot-password | password.request |
| GET | /reset-password/{token} | password.reset |
| POST | /reset-password | password.store |
| PUT | /password | password.update |
| DELETE | /profile | profile.destroy |
| GET | /profile | profile.edit |
| PATCH | /profile | profile.update |
| GET | /verify-email | verification.notice |
| POST | /email/verification-notification | verification.send |
| GET | /verify-email/{id}/{hash} | verification.verify |
| Method | URI | Name |
|---|---|---|
| POST | /api/v1/biometric/attendance | api.biometric.attendance |
| GET | /api/v1/biometric/ping | api.biometric.ping |
| Method | URI | Name |
|---|---|---|
| GET | /feedback/submit/{token} | feedback.public |
| POST | /feedback/submit/{token} | feedback.public.store |
A gold-forward brand palette with semantic theme tokens for light and dark modes.
| Token | Light | Dark | Usage |
|---|---|---|---|
canvas | #F1F5F9 | #0B1220 | App background |
surface | #F8FAFC | #151F32 | Muted surface / inputs |
card | #FFFFFF | #111A2B | Card backgrounds |
line / divider | #E2E8F0 | #253149 | Borders |
ink | #101828 | #F8FAFC | Primary text |
subtle | #64748B | #A8B3C7 | Secondary text |
mute | #94A3B8 | #73809A | Tertiary text |
control | #D0D5DD | #344054 | Input & button borders |
Radius scale control 10px · card 14px · panel 18px. Font: Inter.
High-level Eloquent relationship map.
Consistent, reusable Blade building blocks across both portals.
Dark navy sidebar with grouped navigation, sticky topbar, responsive off-canvas drawer, and a nav that adapts per role (client vs SaaS admin).
Theme-aware inputs use the form-control class (bg-surface text-ink border-control) so fields stay legible in both light and dark mode. Validation errors render inline.
Sortable/filterable listings with pagination, status badges, row actions and friendly empty states.
Flash alerts, confirmation modals, status pills, stat cards, and toast-style notifications via vanilla JS.
data-theme / .dark) with all colours sourced from CSS custom properties — never hard-coded per view.Reusable service classes centralise business rules.
Transactional mailables for onboarding, billing, receipts, payroll and support.
The suite proves functionality and multi-tenant isolation.
Passing tests
Assertions
In-memory test DB
Run the suite with php artisan test. Coverage includes cross-tenant isolation across 18 resource types, role boundaries, branch-switch protections, and module-specific feature tests.
GymSync — Gym Management SaaS · Documentation generated for the current build.