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 available
  • past_due - Grace period, services continue during payment retry
  • cancelled - Services stopped immediately
  • incomplete - Initial payment processing
  • trialing - 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>

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 →