SaasBase Integration #
Complete guide to how ServerMonitor integrates with SaasBase for multi-tenant functionality, subscription management, and user organization.
Overview #
ServerMonitor is built as an extension of the SaasBase multi-tenant framework. This deep integration provides:
- Organization isolation - Each organization only sees their own data
- Subscription enforcement - Services tied to active billing
- User management - Role-based access and limits
- Plan restrictions - Features based on subscription level
- Billing integration - Usage tracking and cost management
Architecture Integration #
Plugin Dependency #
Required Dependency:
// Plugin.php
public $require = ['Albrightlabs.SaasBase'];
ServerMonitor cannot function without SaasBase and will fail to load if SaasBase is not installed.
Model Extensions #
Organization Model Extensions:
// Automatically added by ServerMonitor
\Albrightlabs\SaasBase\Models\Organization::extend(function($model) {
// Add relationships
$model->hasMany['servers'] = [
\Albrightlabs\ServerMonitor\Models\Server::class,
'key' => 'organization_id'
];
$model->hasMany['alert_logs'] = [
\Albrightlabs\ServerMonitor\Models\AlertLog::class,
'key' => 'organization_id'
];
$model->hasOne['notificationSettings'] = [
\Albrightlabs\ServerMonitor\Models\NotificationSetting::class,
'key' => 'organization_id'
];
});
Behavior Integration #
Ownable Behavior:
// Server Model
public $implement = ['@'.\Albrightlabs\SaasBase\Behaviors\OwnableBehavior::class];
This ensures all servers are automatically scoped to the current user's organization.
Subscription Management #
Subscription Status Enforcement #
Active Subscription Required:
// Only process servers for organizations with active subscriptions
$servers = Server::whereHas('organization', function($query) {
$query->whereNotNull('selected_plan')
->where('subscription_status', 'active');
})->get();
Subscription States:
active- Full service, all features availablepast_due- Grace period, services continue during payment retrycancelled- Services stopped immediatelyincomplete- Initial payment processingtrialing- Trial period (treated as active)
Grace Period Handling #
Past Due Processing:
// Organizations with past_due status continue receiving service
$organization->canAccessServices(); // Returns true for 'active' and 'past_due'
// Check in job dispatcher
if (!$organization->canAccessServices()) {
return; // Skip monitoring for this organization
}
This allows customers to continue service during payment retry periods, improving customer experience.
Plan-Based Feature Access #
Plan Detection:
// Determine if organization is on premium plan
protected static function isOrganizationPremium($organizationId)
{
$organization = Organization::find($organizationId);
if (!$organization->canAccessServices()) {
return false;
}
$premiumPlans = ['basic', 'pro', 'enterprise'];
return in_array(strtolower($organization->selected_plan), $premiumPlans);
}
Feature Restrictions:
// Check if organization can use SMS notifications
if (!$organization->isServerMonitorOnPaidPlan()) {
// Skip SMS, only send emails
return;
}
User Management Integration #
Organization-Based Access Control #
User-Organization Relationship:
// Users belong to organizations through SaasBase
$user = BackendAuth::getUser();
$organizationId = $user->organization_id;
// Users only see data from their organization
$servers = Server::where('organization_id', $organizationId)->get();
Administrative Privileges:
// Check if user is organization admin
if ($user->is_organization_admin || str_contains($user->email, '@albrightlabs.com')) {
// Allow access to notification settings
return $this->asExtension('FormController')->update($settings->id);
}
User Limit Enforcement #
Plan-Based User Limits:
// Check user limits when adding to organization
$model->bindEvent('model.relation.beforeAdd', function($relationName, $relatedModel) use ($model) {
if ($relationName == 'users') {
$plan = $model->selected_plan ?? 'free';
$userLimit = Config::get('albrightlabs.servermonitor::config.plans.' . $plan . '.user_limit', 1);
if ($userLimit !== 0) { // 0 means unlimited
$currentUserCount = $model->users()->count();
if ($currentUserCount + 1 > $userLimit) {
throw new \ApplicationException(
'User limit reached. Your ' . ucfirst($plan) . ' plan allows a maximum of ' .
$userLimit . ' users. Please upgrade your plan to add more users.'
);
}
}
}
});
User Limit Configuration:
'plans' => [
'free' => ['user_limit' => 1],
'basic' => ['user_limit' => 0], // 0 = unlimited
'pro' => ['user_limit' => 0],
'enterprise' => ['user_limit' => 0],
]
Data Isolation #
Organization Scoping #
Automatic Scoping:
// All server queries automatically scoped by OwnableBehavior
$servers = Server::all(); // Only returns current organization's servers
// For cross-organization access (Albright Labs staff only)
$allServers = Server::withoutGlobalScope('organization')->get();
Dashboard Metrics:
$user = BackendAuth::getUser();
$isAlbrightLabsUser = strpos($user->email, '@albrightlabs.com') !== false;
$baseCondition = $isAlbrightLabsUser
? [] // No restriction for Albright Labs
: ['organization_id' => $user->organization_id]; // Scoped for regular users
$totalServers = Server::where($baseCondition)->count();
Communication Isolation #
Alert Logs Scoping:
// Alert logs automatically include organization_id
$commLog = new AlertLog();
$commLog->server_id = $server->id;
$commLog->organization_id = $organization->id; // Explicit organization assignment
$commLog->save();
Notification Settings:
// Settings are per-organization
NotificationSetting::getForOrganization($user->organization_id);
// Users can only configure notifications for their organization
$settings->getUserOptions(); // Only returns users from same organization
Billing Integration #
Usage Tracking #
SMS Cost Tracking:
// Each SMS logged with cost per organization
$commLog = new AlertLog();
$commLog->organization_id = $organization->id;
$commLog->type = 'sms';
$commLog->rate = AlertLog::getRate('sms'); // From config
$commLog->save();
Cost Reporting:
// Total SMS costs for organization
$smsCost = AlertLog::where('organization_id', $orgId)
->where('type', 'sms')
->sum(DB::raw('CAST(rate AS DECIMAL(10,4))'));
Plan Limit Enforcement #
Server Count Limits:
// Check server limits before creation
protected function checkServerLimit()
{
$organization = Organization::find($this->organization_id);
$plan = strtolower($organization->selected_plan ?? '');
$limit = Server::getServerLimitForPlan($plan);
$currentCount = Server::where('organization_id', $this->organization_id)->count();
return $currentCount < $limit;
}
Feature Access Control:
// Hide SMS options for free plans
public function filterFields($fields, $context)
{
$organization = $this->organization;
$isPaidPlan = $organization->isOnPaidPlan();
if (isset($fields->sms_enabled)) {
$fields->sms_enabled->hidden = !$isPaidPlan;
}
}
Form Extensions #
Organization Management Integration #
Communication Logs Tab:
// Add tab to organization forms
Event::listen('backend.form.extendFields', function($widget) {
if (!$widget->getController() instanceof \Albrightlabs\SaasBase\Controllers\Organizations) {
return;
}
// Add Alert Logs tab for Albright Labs staff
$widget->addTabFields([
'alert_logs' => [
'label' => 'Alert Logs',
'tab' => 'Alert Logs',
'type' => 'relation',
'span' => 'full',
]
]);
});
Cost Summary Display:
// Display SMS costs in organization summary
<?php
$smsCost = \DB::table('alert_logs')
->where('organization_id', $model->id)
->where('type', 'sms')
->sum(\DB::raw('CAST(rate AS DECIMAL(10,4))'));
?>
<p>SMS Costs: $<?= number_format($smsCost, 2) ?></p>
Navigation Integration #
Menu Context #
Contextual Navigation:
// Hide dashboard item if Albrightlabs.Albrightlabs plugin exists
if (!\System\Classes\PluginManager::instance()->hasPlugin('Albrightlabs.Albrightlabs')) {
Event::listen('backend.menu.extendItems', function($manager) {
$manager->removeMainMenuItem('October.Backend', 'dashboard');
});
}
Dashboard Redirect:
// Redirect October dashboard to Servers
Event::listen('backend.page.beforeDisplay', function ($controller, $action, $params) {
if($controller instanceof \Backend\Controllers\Index){
return Redirect::to(Backend::url('albrightlabs/servermonitor/servers'));
}
});
API Integration #
Organization Validation #
API Endpoint Protection:
// Check for active subscriptions before processing
$activeOrganizationsCount = \Albrightlabs\SaasBase\Models\Organization::where('subscription_status', 'active')
->whereNotNull('selected_plan')
->count();
if ($activeOrganizationsCount === 0) {
return response()->json([
'success' => false,
'message' => 'No active subscriptions found'
], 402); // 402 Payment Required
}
Service Availability #
Organization Service Check:
// Method added to Organization model
public function canAccessServices()
{
return in_array($this->subscription_status, ['active', 'past_due', 'trialing']);
}
// Usage in job processing
if (!$server->organization->canAccessServices()) {
Log::info("Skipping server {$server->id} - organization subscription not active");
return;
}
Settings Integration #
Shared Settings Context #
Settings Menu Integration:
public function registerSettings()
{
return [
'notificationsettings' => [
'label' => 'Notifications',
'description' => 'Configure alert preferences.',
'category' => 'Settings', // Shared with SaasBase settings
'icon' => 'icon-bell',
'url' => Backend::url('albrightlabs/servermonitor/notificationsettings/settings'),
]
];
}
Access Control:
// Only organization admins can access notification settings
if (!$user->is_organization_admin && !str_contains($user->email, '@albrightlabs.com')) {
Flash::error('Access denied. You must be an organization administrator.');
return Redirect::to('backend');
}
Migration Considerations #
Existing SaasBase Integration #
Data Migration from SaasBase:
// Migrate notification settings from SaasBase to ServerMonitor
$saasbaseSettings = \Albrightlabs\SaasBase\Models\OrganizationSetting::where('key', 'server_monitor_notifications')->get();
foreach ($saasbaseSettings as $setting) {
$newSetting = new NotificationSetting();
$newSetting->organization_id = $setting->organization_id;
$newSetting->administrators = $setting->value; // Migrate JSON data
$newSetting->save();
}
Version Compatibility #
SaasBase Version Requirements:
- Minimum: SaasBase v2.0.0
- Recommended: Latest stable version
- Features: Requires subscription management and organization behaviors
Compatibility Checks:
// Check SaasBase version compatibility
$saasbaseVersion = \Albrightlabs\SaasBase\Plugin::getVersion();
if (version_compare($saasbaseVersion, '2.0.0', '<')) {
throw new \Exception('ServerMonitor requires SaasBase v2.0.0 or higher');
}
Troubleshooting Integration Issues #
Common Problems #
Organizations Not Found:
// Debug organization relationships
$user = BackendAuth::getUser();
echo "User Org ID: " . $user->organization_id . "\n";
$org = \Albrightlabs\SaasBase\Models\Organization::find($user->organization_id);
echo "Org Name: " . $org->name . "\n";
echo "Subscription: " . $org->subscription_status . "\n";
Permission Issues:
// Check user permissions and organization admin status
$user = BackendAuth::getUser();
echo "Is Org Admin: " . ($user->is_organization_admin ? 'Yes' : 'No') . "\n";
echo "Email: " . $user->email . "\n";
echo "Organization: " . $user->organization->name . "\n";
Subscription Status:
// Verify subscription enforcement
$activeOrgs = \Albrightlabs\SaasBase\Models\Organization::where('subscription_status', 'active')->count();
echo "Active Organizations: " . $activeOrgs . "\n";
$servers = Server::whereHas('organization', function($q) {
$q->where('subscription_status', 'active');
})->count();
echo "Servers with Active Subscriptions: " . $servers . "\n";
Data Consistency #
Orphaned Records:
-- Find servers without organizations
SELECT s.id, s.title, s.organization_id
FROM servers s
LEFT JOIN organizations o ON s.organization_id = o.id
WHERE o.id IS NULL;
-- Find notification settings without organizations
SELECT ns.id, ns.organization_id
FROM servermonitor_notification_settings ns
LEFT JOIN organizations o ON ns.organization_id = o.id
WHERE o.id IS NULL;
Cleanup Scripts:
// Remove orphaned servers
Server::whereNotIn('organization_id',
\Albrightlabs\SaasBase\Models\Organization::pluck('id')
)->delete();
// Remove orphaned notification settings
NotificationSetting::whereNotIn('organization_id',
\Albrightlabs\SaasBase\Models\Organization::pluck('id')
)->delete();
Best Practices #
Development #
Always Check Organization Context:
// Never assume organization exists
$user = BackendAuth::getUser();
if (!$user || !$user->organization_id) {
throw new \Exception('User must belong to an organization');
}
Respect Subscription Status:
// Always check subscription before processing
if (!$organization->canAccessServices()) {
Log::info("Organization {$org->id} subscription not active, skipping");
return;
}
Use SaasBase Behaviors:
// Leverage existing SaasBase functionality
public $implement = ['@'.\Albrightlabs\SaasBase\Behaviors\OwnableBehavior::class];
Production #
Monitor Integration Health:
- Check for orphaned records regularly
- Verify subscription status enforcement
- Monitor organization-user relationships
- Validate plan limit enforcement
Security Considerations:
- Never bypass organization scoping
- Always validate user permissions
- Audit cross-organization data access
- Log administrative actions
Previous: ← Server Management | Next: Security →