yungifez/skuul
Skuul is an open-source Laravel school management system for handling students, staff, classes, attendance, exams, and fees. It provides a clean admin dashboard and practical tools to streamline daily school operations, with a flexible, self-hosted setup for schools.
Skuul is a full Laravel application, not a composer require-able library. You adopt it by forking/cloning the repo and running it as your base, so “getting started” means standing up the app and learning its domain model.
Minimal path:
git clone https://github.com/yungifez/skuul.git
cd skuul
composer install
npm install && npm run build
cp .env.example .env
php artisan key:generate
.env for your database (MySQL/Postgres), mail, and filesystem, then:
php artisan migrate --seed
php artisan storage:link
php artisan serve
Where to look first (in rough order of value):
routes/ — reveals the actual module surface (students, teachers, classes, attendance, exams, fees, reports).app/Models/ — the domain core: School, Session/AcademicYear, Class, Section, Student, Guardian, Teacher, Exam, Attendance, Fee.database/migrations/ and database/seeders/ — how multi-school scoping (school_id) and roles are modeled.app/Http/Middleware/ — how the “current school” context is resolved per request.resources/views/ (and any Livewire components) — the UI layer you’ll be customizing.First real use case: run it locally as a single-school install, seed demo data, then walk one student through the full lifecycle — enroll → mark attendance → create an exam → enter marks → generate a report card → record a fee payment. That exercise touches every subsystem you’ll be extending and exposes the session/school scoping rules early.
Treat it as a fork, not a dependency. Keep your customizations in your own namespaces (App\Modules\..., App\Services\...) and prefer new migrations/views over editing upstream files, so you can still pull security fixes from the repo.
Multi-school scoping is the central pattern. Most domain tables carry a school_id (and often session_id). Follow the existing convention religiously:
school_id foreign key and an index.auth()->user()->school_id ad hoc in every controller.// Illustrative shape of a school-scoped model
class Hostel extends Model
{
protected $fillable = ['school_id', 'name'];
protected static function booted(): void
{
static::addGlobalScope('school', fn ($q) =>
$q->where('school_id', app(SchoolContext::class)->id())
);
}
}
Adding a module (e.g., library, hostel, transport):
school_id (+ session_id if it’s year-bound).Extending existing entities: add columns via a new migration with a later timestamp; never edit the original migration once it’s shipped to other schools. Use model events (StudentCreated, fee-paid, attendance-marked) as the hook for notifications, SMS/email, and audit logs rather than stuffing logic into controllers.
Bulk onboarding: the highest-leverage day-to-day tool is a CSV import artisan command (php artisan school:import-students {file}) that wraps the same services the UI uses, so validation and school scoping are identical.
Year rollover: many queries implicitly depend on the “current session.” When you add features, be explicit about whether you query the active session or accept a session parameter — this is where most custom modules silently break.
DB::table(), report export, or API endpoint you add bypasses global scopes. Test every new listing endpoint with two schools in the database and assert isolation.php artisan permission:cache-reset (or the app’s equivalent) or users will see stale access.student, class, section; ensure indexes on school_id, session_id, and foreign keys; paginate everything.php artisan storage:link and the correct FILESYSTEM_DISK; a missing link is a common “broken image” report.storage/logs/laravel.log before assuming a scoping bug.How can I help you explore Laravel packages today?