Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Elian-D/ORVIAN/llms.txt

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

ORVIAN’s access control is built on spatie/laravel-permission with teams = true enabled. In this configuration the “team” concept maps 1-to-1 with school_id, meaning every role and permission check is automatically scoped to the school that owns the current request. Global roles — those held by platform administrators — have school_id = null and exist outside any team. Tenant roles are cloned per school at the end of the onboarding wizard so that each school starts with an independent, customisable set of roles and permissions.

Global Roles (Platform Admin)

Global roles are held by users whose school_id is null. They operate entirely within the /admin/* route group and are never visible to school-level users.
RolePurpose
OwnerFull platform access. Holds the viewPulse gate (grants access to Laravel Pulse monitoring via Gate::define('viewPulse', fn($user) => $user->hasRole('Owner'))). Can impersonate any school.
TechnicalSupportRead/write access to school configuration and user management across all tenants. Can enter a school’s context via session('impersonated_school_id').
AdministrativePlatform-level administrative access for operational tasks (billing, plan management, etc.).
Global roles are defined in seeders and are never cloned. Because school_id = null on these role records, Spatie’s team isolation keeps them completely separate from any school’s role table.

Tenant Roles (School Panel)

Tenant roles are created per school by SchoolRoleService::seedDefaultRoles() during onboarding. Each role is cloned from a global reference role (also school_id = null) so every school starts with the same baseline permissions. Future customisation per school (adding/removing permissions from a role) does not affect any other school. The base roles seeded for every school are:
RoleTypical user
School PrincipalThe school Director — broadest permissions within the school panel
Academic CoordinatorManages academic structure, enrolment, and teacher assignments
TeacherClassroom attendance, roll call, subject-level views
SecretaryAdministrative staff — student records, excuses
StudentMinimal read-only access to personal records
StaffGeneral support staff
The seeding process temporarily sets setPermissionsTeamId(null) to load the global reference roles (bypassing team scoping), then switches to setPermissionsTeamId($school->id) to create the tenant copies and sync their permissions:
// app/Services/School/SchoolRoleService.php
public function seedDefaultRoles(School $school): void
{
    // Phase 1 — READ: disable team filter to see global reference roles
    setPermissionsTeamId(null);

    $globalRoles = Role::withoutGlobalScope(SchoolScope::class)
        ->whereIn('name', self::BASE_ROLES)
        ->whereNull('school_id')
        ->with('permissions')
        ->get();

    // Phase 2 — WRITE: activate school's team scope
    setPermissionsTeamId($school->id);

    foreach (self::BASE_ROLES as $roleName) {
        $globalRole = $globalRoles->firstWhere('name', $roleName);
        $color      = $globalRole ? $globalRole->color : '#64748B';

        $tenantRole = Role::firstOrCreate(
            ['name' => $roleName, 'guard_name' => 'web', 'school_id' => $school->id],
            ['color' => $color, 'is_system' => true]
        );

        if ($globalRole && $tenantRole->wasRecentlyCreated) {
            $this->clonePermissions($globalRole, $tenantRole);
        }
    }
}
Role display names shown in the UI (badges, role pickers) are resolved through the roles.{name}.name translation key via the $role->display_name accessor.

Permission Groups

Permissions are organised into PermissionGroup records. A group has:
  • name — human-readable label (e.g., "Asistencia Plantel")
  • slug — machine key used for translation lookups (permissions.groups.{slug})
  • context — either 'global' or 'tenant'
  • order — integer for display ordering in the role editor
// app/Models/PermissionGroup.php
const CONTEXT_GLOBAL = 'global';
const CONTEXT_TENANT = 'tenant';

// Scopes
->scopeTenant($query)  // where('context', 'tenant')
->scopeGlobal($query)  // where('context', 'global')
->scopeOrdered($query) // orderBy('order')
Each Permission model extends Spatie’s base and adds a group_id foreign key, exposing a group() BelongsTo relation. The Permission::roles() relation is overridden to call ->withoutGlobalScope(SchoolScope::class), ensuring the Spatie permission cache can resolve global roles (like Owner) even when a tenant scope is active.

Key Attendance Permissions

These permissions are checked on routes under /app/attendance/*:
Permission nameDescription
attendance_plantel.viewView the campus-wide attendance dashboard and session history
attendance_plantel.recordRecord an individual campus entry via QR scanner
attendance_plantel.open_sessionOpen or close the daily campus attendance session; access manual entry
attendance_plantel.reportsAccess attendance analytics and export reports
attendance_plantel.verifyAudit completed sessions (/attendance/audit/{sessionId})
attendance_classroom.recordRun a classroom roll-call session (teacher’s pase de lista)
attendance_classroom.viewView classroom attendance history for assigned sections
excuses.viewView and manage student attendance excuse records

Key Academic Permissions

These permissions gate routes under /app/academic/*:
Permission nameDescription
students.viewList and view student profiles
students.createCreate a new student record
students.editEdit an existing student; access the enrolment hub and biometric kiosk
students.importImport students via Excel wizard; access the print/QR manager
teachers.viewList and view teacher profiles
teachers.createCreate a new teacher record
teachers.editEdit an existing teacher profile
teachers.assign_subjectsManage a teacher’s subject and section assignments
settings.viewView academic structure (courses, sections, shifts)
settings.updateModify academic structure configuration

Checking Permissions in Routes

Route-level permission gates use Laravel’s built-in can: middleware alias, which delegates to Spatie under the hood. The team ID is already set by IdentifyTenant before route matching, so the check is automatically scoped to the current school. Attendance routes:
// routes/app/attendance.php
Route::get('/dashboard', AttendanceDashboard::class)
    ->middleware('can:attendance_plantel.reports')
    ->name('dashboard');

Route::get('/reports', AttendanceReports::class)
    ->middleware('can:attendance_plantel.reports')
    ->name('reports');

Route::middleware('can:attendance_plantel.record')->group(function () {
    Route::get('/scanner', AttendanceScanner::class)->name('scanner');
});

Route::middleware('can:attendance_plantel.open_session')->group(function () {
    Route::get('/session', AttendanceSessionManager::class)->name('session');
    Route::get('/manual', ManualAttendance::class)->name('manual');
});

Route::get('/audit/{sessionId}', AttendanceAudit::class)
    ->middleware('can:attendance_plantel.verify')
    ->name('audit');

Route::prefix('excuses')->name('excuses.')->group(function () {
    Route::get('/', ExcuseIndex::class)
        ->middleware('can:excuses.view')
        ->name('index');
});
Academic routes:
// routes/app/academic.php
Route::middleware('can:students.view')->group(function () {
    Route::get('/students', StudentIndex::class)->name('students.index');
    Route::get('/students/create', StudentForm::class)
        ->middleware('can:students.create')
        ->name('students.create');
    Route::get('/students/import', StudentImportWizard::class)
        ->middleware('can:students.import')
        ->name('students.import');
    Route::get('/students/{student}/edit', StudentForm::class)
        ->middleware('can:students.edit')
        ->name('students.edit');
});

Route::middleware('can:teachers.view')->group(function () {
    Route::get('/teachers', TeacherIndex::class)->name('teachers.index');
    Route::get('/teachers/create', TeacherForm::class)
        ->middleware('can:teachers.create')
        ->name('teachers.create');
    Route::get('/teachers/{teacher}/assignments', TeacherAssignments::class)
        ->middleware('can:teachers.assign_subjects')
        ->name('teachers.assignments');
});

The trans_permission() Helper

Technical permission names like attendance_plantel.open_session are not shown directly to end users. The trans_permission() global helper (autoloaded from app/Helpers/PermissionHelper.php) translates them to human-readable labels using the lang/es/permissions.php language file:
// app/Helpers/PermissionHelper.php
function trans_permission(string $name, string $key = 'label'): string
{
    // Builds: "permissions.attendance_plantel.open_session.label"
    $langKey = "permissions.{$name}.{$key}";

    if (Lang::has($langKey)) {
        return __($langKey);
    }

    // Graceful fallback: "Attendance plantel open session"
    return ucfirst(str_replace(['.', '_'], ' ', $name));
}
Parameters:
ParameterValuesDescription
$namee.g., 'students.view'The Spatie permission name
$key'label' (default) or 'description'Which translation string to return
Usage examples:
{{-- In Blade --}}
{{ trans_permission('attendance_plantel.record') }}
{{-- → "Registrar asistencia de plantel" --}}

{{ trans_permission('students.import', 'description') }}
{{-- → "Importar estudiantes desde un archivo Excel" --}}
The helper is safe to call anywhere — if the translation key does not exist it falls back to a cleaned version of the raw permission string, so the UI never surfaces raw dot-notation to end users.
SchoolScope is automatically bypassed for all routes under admin/*. The Role model registers an anonymous global scope that calls ->withoutGlobalScope(SchoolScope::class) whenever request()->is('admin/*') is true. This means permission checks, role listings, and role assignments performed in the admin panel correctly operate across all schools without any manual scope bypass.

Build docs developers (and LLMs) love