Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Arthurr23/XHealtXperience/llms.txt

Use this file to discover all available pages before exploring further.

XHealtXperience is built on a two-database, path-based multi-tenancy model powered by stancl/tenancy v3. Every clinic (tenant) lives in its own isolated database, accessed transparently by the framework once a request’s URL prefix is matched to a known tenant ID. This page explains how the central and tenant planes are structured, how requests flow through the system, and how the key packages fit together.

The Two-Database Model

The application maintains two distinct database tiers at all times.

Central Database

The central database is the single source of truth for platform-level data. It contains:
  • tenants — one row per clinic, storing id (UUID), nombre_clinica, codigo_clinica, plan, limite_cuentas, estado, and fecha_inicio_suscripcion
  • domains — path prefix mappings from tenant IDs to their routing segment
  • users — the Super Admin account (and any future central users)
  • sessions, jobs, cache, failed_jobs — Laravel infrastructure tables
  • Spatie Permission tables (roles, permissions, model_has_roles, …) — scoped to the central guard for the Super Admin role
The central database defaults to SQLite in local development (database/database.sqlite). In production, the config/tenancy.php file sets 'central_connection' => 'mysql'.

Tenant Databases (One per Clinic)

When the Super Admin provisions a new clinic via POST /panel-global/clinicas, stancl/tenancy automatically creates a fresh database for that tenant and runs the tenant migration set located in database/migrations/tenant/. Each tenant database contains:
  • users — clinic staff and patient accounts (completely separate from central users)
  • patients (pacientes), expedientes_clinicos — clinical records
  • citas — appointment scheduling
  • servicios, lineas_servicio, paciente_servicio_etapas — service catalogue and stage tracking
  • salas, procedimientos_quirurgicos — operating room and surgical procedure management
  • audit_logs (bitacora) — immutable audit trail
  • tenant_settings — per-clinic security and scheduling configuration
  • Spatie Permission tables — roles (Administrador Clinica, Doctor, Recepcion, Enfermero, Auditor, Paciente, …) scoped to that tenant’s guard
Tenant databases follow the naming convention defined in config/tenancy.php:
// config/tenancy.php
'database' => [
    'central_connection' => 'mysql',
    'prefix' => 'tenant',
    'suffix' => '.sqlite',   // e.g. tenant550e8400-e29b-41d4-a716-446655440000.sqlite
],
Cache, filesystem storage (uploaded IDs, patient signatures), and queued jobs are all automatically scoped per tenant by the tenancy bootstrappers (CacheTenancyBootstrapper, FilesystemTenancyBootstrapper, QueueTenancyBootstrapper). No manual namespacing is required in application code.

Request Flow

Every HTTP request passes through the same pipeline before reaching a controller. The diagram below shows the key decision points for a tenant request:
Browser


Laravel HTTP Kernel
  │  (runs registered global + web middleware stack)


InitializeTenancyByPath          ← reads segment(1) of the URL path
  │  e.g. /{tenantId}/dashboard
  │  • looks up tenant in central DB by ID
  │  • swaps the active DB connection to the tenant database
  │  • boots: DatabaseTenancyBootstrapper
  │            CacheTenancyBootstrapper
  │            FilesystemTenancyBootstrapper
  │            QueueTenancyBootstrapper


auth middleware                   ← checks the tenant's own users table


EnsureTwoFactorAuthenticated      ← alias: two_factor
  │  • blocks access if 2FA not yet verified this session
  │  • redirects to /{tenantId}/two-factor/challenge


CheckInactivity                   ← alias: check_inactivity
  │  • logs out sessions idle beyond the configured threshold


role middleware (Spatie)          ← optional, on role-restricted routes


Controller (Laravel)


Inertia::render(...)              ← returns JSON for XHR, full HTML for first visit


React 18 (client-side)            ← hydrates / updates the page component
For central routes (e.g. GET /panel-global/dashboard), InitializeTenancyByPath is never invoked and all database queries run against the central connection.

Routing Architecture

Routes are split across two files that serve completely different database contexts.

Central Routes — routes/web.php

Handles all requests that do not belong to a tenant clinic:
// routes/web.php (excerpt)

// Welcome page
Route::get('/', fn () => Inertia::render('Welcome', [...]);

// Central 2FA challenge (for Super Admin)
Route::middleware('auth')->group(function () {
    Route::get('/two-factor/challenge', [TwoFactorController::class, 'showChallenge']);
    Route::post('/two-factor/challenge', [TwoFactorController::class, 'verify']);
});

// Super Admin panel — protected by auth + two_factor + role:Super Admin
Route::middleware(['auth', 'two_factor', 'role:Super Admin'])
    ->prefix('panel-global')
    ->group(function () {
        Route::get('/dashboard',  [SuperAdminController::class, 'dashboard']);
        Route::post('/clinicas',  [SuperAdminController::class, 'store']);
        // ... clinic CRUD, user management, 2FA setup
    });

Tenant Routes — routes/tenant.php

All tenant routes share a single top-level middleware group that runs InitializeTenancyByPath first:
// routes/tenant.php (excerpt)

Route::middleware([
    'web',
    \Stancl\Tenancy\Middleware\InitializeTenancyByPath::class,
])->group(function () {

    // Guest routes — no auth required
    Route::get('/login',  [AuthenticatedSessionController::class, 'create']);
    Route::post('/login', [AuthenticatedSessionController::class, 'store']);

    // 2FA challenge — auth but NOT yet two_factor verified
    Route::middleware('auth')->group(function () {
        Route::get('/two-factor/challenge',  [TwoFactorController::class, 'showChallenge']);
        Route::post('/two-factor/challenge', [TwoFactorController::class, 'verify']);
    });

    // Protected dashboard + most features — auth + two_factor + check_inactivity
    Route::middleware(['auth', 'two_factor', 'check_inactivity'])->group(function () {
        Route::get('/dashboard', [DashboardController::class, 'index']);
        Route::resource('pacientes', PacienteController::class);
        Route::resource('citas', CitaController::class)->only(['index','create','store']);
        // ... services, surgeries, audit log, etc.
    });

    // Role-restricted routes
    Route::middleware(['auth', 'two_factor', 'check_inactivity', 'role:Administrador Clinica'])
        ->group(function () {
            Route::resource('usuarios', UserController::class);
            // ... service catalogue admin, OR management
        });
});
The 2FA challenge routes are intentionally placed outside the two_factor middleware group. Placing them inside would create an infinite redirect loop because the two_factor middleware would block access to the very page needed to complete 2FA verification.

Route Summary

URL PatternRoute FileAuth RequiredMiddleware
/web.phpNoweb
/loginweb.phpNo (guest)web, guest
/panel-global/*web.phpYesauth, two_factor, role:Super Admin
/{tenantId}/logintenant.phpNoweb, InitializeTenancyByPath
/{tenantId}/two-factor/challengetenant.phpAuth onlyweb, InitializeTenancyByPath, auth
/{tenantId}/dashboardtenant.phpYes, auth, two_factor, check_inactivity
/{tenantId}/usuariostenant.phpYes + role, auth, two_factor, role:Administrador Clinica

Middleware Reference

The following custom and aliased middleware are registered in bootstrap/app.php:
AliasClassPurpose
two_factorApp\Http\Middleware\EnsureTwoFactorAuthenticatedBlocks requests if 2FA challenge not yet passed
check_inactivityApp\Http\Middleware\CheckInactivityLogs out sessions idle beyond tenant threshold
roleSpatie\Permission\Middleware\RoleMiddlewareRestricts access to users with a specific role
permissionSpatie\Permission\Middleware\PermissionMiddlewareRestricts access to users with a specific permission
role_or_permissionSpatie\Permission\Middleware\RoleOrPermissionMiddlewareCombines role and permission checks
InitializeTenancyByPath is given the highest priority in the middleware stack so it always runs before session, CSRF, and auth middleware — ensuring the correct tenant database is active when those layers execute.

Frontend Architecture

The frontend is a React 18 SPA driven by Inertia.js 2. There is no separate API layer — controllers return Inertia::render() calls which Inertia serialises as JSON for client-side navigation and as full HTML for the first page load.
// vite.config.js
export default defineConfig({
    server: {
        host: '127.0.0.1',
        port: 5173,
        strictPort: true,
        cors: true,
    },
    plugins: [
        laravel({ input: 'resources/js/app.jsx', refresh: true }),
        react(),
    ],
});
Tailwind CSS v3 is integrated via PostCSS (postcss.config.js) with a standard tailwind.config.js configuration file. Key frontend packages:
PackageVersionRole
react / react-dom^18.2UI rendering
@inertiajs/react^2.0Client-side routing and server-driven page props
tailwindcss^3.2Utility-first CSS (PostCSS integration)
@tailwindcss/forms^0.5Form styling plugin for Tailwind CSS
@headlessui/react^2.0Accessible UI primitives (modals, dropdowns, etc.)
tightenco/ziggy^2.0Named Laravel routes available in JavaScript
qrcode / jsqr^1.xQR code generation and scanning for 2FA setup
axios^1.11HTTP client for non-Inertia requests (CSRF, etc.)
Vite 7 (laravel-vite-plugin) handles asset bundling and Hot Module Replacement. The stancl/tenancy ViteBundler feature (enabled in config/tenancy.php) ensures Vite’s asset manifest is resolved correctly inside tenant contexts.

Queue Worker Requirement

Several operations in XHealtXperience are dispatched asynchronously to the database queue:
  • Tenant database creation and initial migration when a new clinic is provisioned
  • Any QueueTenancyBootstrapper-managed jobs that must run inside a specific tenant context
The dev script starts the worker automatically:
php artisan queue:listen --tries=1 --timeout=0
In production, this must be managed by a process supervisor (e.g. Supervisor, systemd, or Laravel Octane). Without an active worker, tenant provisioning will stall silently.

Tenancy Bootstrappers

When InitializeTenancyByPath initialises a tenant, it fires the bootstrappers listed in config/tenancy.php. Each bootstrapper makes a Laravel feature tenant-aware for the duration of that request:
BootstrapperEffect
DatabaseTenancyBootstrapperSwitches the default DB connection to the tenant’s database
CacheTenancyBootstrapperPrefixes all cache keys with tenant{id} to prevent cross-tenant leaks
FilesystemTenancyBootstrapperScopes storage_path() and disk roots to storage/app/tenantXXX/
QueueTenancyBootstrapperInjects tenant context into queued jobs so they run in the right DB

Explore Further

Tenancy Overview

Detailed guide to tenant lifecycle management: provisioning, migrations, seeding, and deletion.

Local Development Setup

Advanced local configuration: MySQL instead of SQLite, environment variables, and debugging tools.

Build docs developers (and LLMs) love