Module Tenant Compatibility Rules
This guide defines how to build new modules so they work in all cases:
- LMS only (SaaS module not installed)
- SaaS enabled with separate database mode
- SaaS enabled with single shared database mode
Golden Rules
- SaaS is optional.
- Separate DB mode remains the default safe production path.
- Single shared DB mode must isolate tenant rows using tenant_id.
- New modules should not require manual per-model SaaS wiring.
- Do not add
TenantOwnedModelmanually to each model; global auto-scope handles this when SaaS is enabled.
Module.json Rules
By default, modules are treated as tenant-capable.
Use these module.json keys to define behavior:
tenant_migrations(existing key)trueor omitted: module is tenant-capable.false: module is central-only.
tenant_auto_scope(new)true(default): automatic tenant row scoping in single DB mode.false: disable automatic row scoping (for central-only or custom/hybrid manual control).
tenant_scope_exempt_tables(new)- list of tables that must never receive automatic tenant_id behavior.
tenant_scope_tables(new, optional hybrid allow-list)- if provided, only listed tables are auto-scoped for that module.
single_db_exempt_tables(new alias)- alias for
tenant_scope_exempt_tablesused by migration table injection logic.
- alias for
What Is Global Now
When SaaS module is enabled:
- Global auto model scoping is registered from SaaS provider.
- It reads module rules from each
Modules/{Module}/module.json. - It applies tenant_id query scope and auto-fill only in:
- single shared DB mode,
- tenant-initialized request context,
- tables that contain
tenant_id.
Manual trait wiring is no longer needed for module models.
Implementation location:
- Global model scope injection is implemented inside SaaS module.
- Core modules (LMS, Roles, ModuleManager, etc.) stay clean and SaaS-independent.
Central requests are not blocked because no scope is applied when tenancy is not initialized.
Migration Rule (tenant_id injection)
The SaaS migration that adds tenant_id columns now merges exemptions from:
- hard defaults (infrastructure + SaaS central tables),
tenancy.single_db.exempt_tablesconfig,- each module.json key:
tenant_scope_exempt_tablessingle_db_exempt_tables
This lets hybrid modules control table-level behavior without changing core code.
Important:
- The schema preparation migration lives in SaaS module, not LMS core.
- Therefore, non-SaaS installations are not affected.
Optional SaaS Safety
If SaaS/stancl is absent, tenant helpers/classes may not exist.
Always guard tenancy usage with:
function_exists('tenancy')function_exists('tenant')class_exists(...)for stancl classes
Current LMS implementation already follows this in key runtime paths.
Quick Examples
Central-only module
{
"name": "Reporting",
"tenant_migrations": false,
"tenant_auto_scope": false,
"single_db_exempt_tables": ["reporting_snapshots", "reporting_exports"]
}Tenant-capable module (default)
{
"name": "Assignments"
}Hybrid module
{
"name": "Analytics",
"tenant_auto_scope": true,
"tenant_scope_exempt_tables": ["analytics_global_dictionary"],
"tenant_scope_tables": ["analytics_events", "analytics_reports"]
}Recommendation
- Keep separate mode as default.
- Build modules with these rules from day one.
- Treat single mode as first-class only after tenant_id coverage and indexes are complete.
Base Model Question (EdulabModel)
Using one shared base model can work for plain Eloquent models, but this codebase has mixed bases:
ModelAuthenticatable- module-specific trait stacks
So forcing every class to extend one EdulabModel would add migration effort and risk regressions.
Current standard in this project:
- Keep existing model inheritance.
- Use global SaaS auto-scope manager for tenant row isolation logic.
- Control behavior through
module.jsonpolicies.
No core-module base class migration is required.
This remains aligned with international multi-tenant patterns because scope behavior is centralized, explicit, and mode-aware.