Files
website/KASM_AND_MULTITENANCY.md
Claude EZSCALE 052f651ee1 Init Commit
2026-02-09 01:05:29 -05:00

28 KiB

Kasm Workspaces & Multi-Tenancy Implementation

Overview

This document details the implementation plan for:

  1. Kasm Workspaces - Cloud desktop/workspace service with hourly billing
  2. Multi-Tenancy - White-label reseller platform using Tenancy for Laravel

Part 1: Kasm Workspaces Integration

What is Kasm Workspaces?

Kasm Workspaces provides streaming containerized apps and desktops to end-users. It's perfect for:

  • Developer Workspaces: Pre-configured dev environments (VS Code, IDEs, terminals)
  • Business Workspaces: Office apps, browsers, secure remote work environments

Official Docs: https://docs.kasm.com/ Developer API: https://docs.kasm.com/docs/latest/developers/developer_api/

Service Offerings

Workspace Type Use Case Target Customers
Developer Basic 2 vCPU, 4GB RAM, Linux with VS Code Freelance developers, students
Developer Pro 4 vCPU, 8GB RAM, multiple IDEs, Docker Professional developers, small teams
Developer Enterprise 8 vCPU, 16GB RAM, full dev stack Development teams, agencies
Business Basic 2 vCPU, 4GB RAM, Browser + Office apps Remote workers, contractors
Business Pro 4 vCPU, 8GB RAM, Full office suite Business users, managers
Business Enterprise 8 vCPU, 16GB RAM, Custom apps Executives, power users

Provisioning Strategy

Fully Automated Provisioning

  1. Customer selects workspace type and template
  2. Payment processed (either subscription or credits added)
  3. Kasm API called to create workspace
  4. Workspace URL and credentials sent via email
  5. Customer can access workspace immediately

Kasm API Integration

Key API Endpoints

POST /api/public/create_workspace
Parameters:
- workspace_type (developer_basic, business_pro, etc.)
- user_id
- session_duration (optional)

Response:
{
  "workspace_id": "abc-123",
  "access_url": "https://kasm.ezscale.cloud/workspace/abc-123",
  "username": "user@email.com",
  "password": "generated_password"
}

POST /api/public/destroy_workspace
Parameters:
- workspace_id

GET /api/public/workspace_status
Parameters:
- workspace_id

Response:
{
  "status": "running",
  "uptime_seconds": 3600,
  "cpu_usage_percent": 45,
  "ram_usage_mb": 2048
}

POST /api/public/stop_workspace
POST /api/public/start_workspace

Authentication

  • API Key authentication via headers
  • X-API-Key: <key> and X-API-Secret: <secret>
  • Keys generated in Kasm admin panel

Database Schema

kasm_workspaces table:
├── id
├── user_id
├── service_id (links to main services table)
├── workspace_type (developer_basic, business_pro, etc.)
├── kasm_workspace_id (Kasm's internal ID)
├── access_url
├── username (workspace login)
├── password_encrypted
├── status (provisioning, running, stopped, terminated)
├── vcpu (2, 4, 8)
├── ram_mb (4096, 8192, 16384)
├── template_name (Ubuntu with VS Code, Windows 11 Business, etc.)
├── created_at
├── provisioned_at
├── last_started_at
├── last_stopped_at
├── terminated_at

kasm_usage_sessions table:
├── id
├── kasm_workspace_id
├── started_at
├── stopped_at (nullable - ongoing session)
├── duration_seconds (calculated)
├── duration_billable_seconds (rounded to 15-min increments)
├── cost_per_hour (rate at time of use)
├── total_cost (duration_billable_seconds / 3600 * cost_per_hour)
├── invoice_id (nullable - which invoice this was billed on)
├── created_at

kasm_workspace_templates table:
├── id
├── name (Ubuntu 22.04 + VS Code)
├── description
├── workspace_type (developer, business)
├── kasm_template_id (Kasm's image ID)
├── icon_url
├── vcpu_default
├── ram_mb_default
├── preinstalled_apps (JSON array)
├── status (active, deprecated)
├── sort_order

Hourly Billing Model

Billing Calculation

  • 15-minute increments (round up)
  • Examples:
    • 8 minutes = 15 minutes = $0.05 (if $0.20/hour)
    • 22 minutes = 30 minutes = $0.10
    • 1 hour 5 minutes = 1 hour 15 minutes = $0.25

Pricing Structure

Workspace Type vCPU RAM Price per Hour
Developer Basic 2 4GB $0.15
Developer Pro 4 8GB $0.30
Developer Enterprise 8 16GB $0.60
Business Basic 2 4GB $0.20
Business Pro 4 8GB $0.40
Business Enterprise 8 16GB $0.80

Note: Pricing subject to adjustment based on Kasm licensing costs and infrastructure overhead.

Hybrid Billing Approach

  • Real-time tracking: Dashboard shows current running cost
  • Monthly invoicing: All usage invoiced at end of billing cycle
  • Running total: Customer sees "Current month usage: $47.35" in real-time
  • Low balance alerts: Warn when usage approaching credit limit

Implementation Flow

1. Customer Orders Workspace

// routes/web.php
Route::post('/kasm/order', [KasmController::class, 'createOrder'])
    ->middleware(['auth', 'verified']);

// app/Http/Controllers/KasmController.php
public function createOrder(Request $request)
{
    $plan = Plan::findOrFail($request->plan_id);

    // Validate customer has credits or payment method
    if ($user->account_credits < 5.00 && !$user->hasPaymentMethod()) {
        return back()->with('error', 'Add credits or payment method first');
    }

    // Create service record
    $service = Service::create([
        'user_id' => $user->id,
        'plan_id' => $plan->id,
        'service_type' => 'kasm_workspace',
        'status' => 'provisioning',
    ]);

    // Queue provisioning job
    ProvisionKasmWorkspace::dispatch($service);

    return redirect()->route('services.show', $service)
        ->with('success', 'Workspace is being provisioned...');
}

2. Provisioning Job

// app/Jobs/ProvisionKasmWorkspace.php
class ProvisionKasmWorkspace implements ShouldQueue
{
    public function handle()
    {
        $kasmService = app(KasmProvisioningService::class);

        try {
            // Call Kasm API to create workspace
            $result = $kasmService->createWorkspace([
                'workspace_type' => $this->service->plan->kasm_workspace_type,
                'vcpu' => $this->service->plan->vcpu,
                'ram_mb' => $this->service->plan->ram_mb,
                'template' => $this->service->plan->kasm_template_id,
            ]);

            // Store workspace details
            KasmWorkspace::create([
                'service_id' => $this->service->id,
                'user_id' => $this->service->user_id,
                'workspace_type' => $this->service->plan->kasm_workspace_type,
                'kasm_workspace_id' => $result['workspace_id'],
                'access_url' => $result['access_url'],
                'username' => $result['username'],
                'password_encrypted' => encrypt($result['password']),
                'status' => 'running',
                'vcpu' => $this->service->plan->vcpu,
                'ram_mb' => $this->service->plan->ram_mb,
                'provisioned_at' => now(),
                'last_started_at' => now(),
            ]);

            // Start usage session
            KasmUsageSession::create([
                'kasm_workspace_id' => $workspace->id,
                'started_at' => now(),
                'cost_per_hour' => $this->service->plan->price_per_hour,
            ]);

            // Update service status
            $this->service->update([
                'status' => 'active',
                'provisioned_at' => now(),
            ]);

            // Send email with credentials
            Mail::to($this->service->user)->send(
                new KasmWorkspaceProvisioned($workspace)
            );

        } catch (\Exception $e) {
            // Handle provisioning failure
            Log::error('Kasm provisioning failed', [
                'service_id' => $this->service->id,
                'error' => $e->getMessage(),
            ]);

            $this->service->update(['status' => 'failed']);

            // Alert admin via Discord
            app(DiscordNotificationService::class)->sendAlert([
                'title' => 'Kasm Provisioning Failed',
                'message' => "Service #{$this->service->id} failed to provision",
                'error' => $e->getMessage(),
            ]);
        }
    }
}

3. Usage Tracking

// app/Console/Commands/TrackKasmUsage.php
// Run every 15 minutes via cron

class TrackKasmUsage extends Command
{
    public function handle()
    {
        $activeWorkspaces = KasmWorkspace::where('status', 'running')->get();

        foreach ($activeWorkspaces as $workspace) {
            // Check if workspace is still running via API
            $status = app(KasmProvisioningService::class)
                ->getWorkspaceStatus($workspace->kasm_workspace_id);

            if ($status['status'] === 'stopped') {
                // Workspace was stopped, close usage session
                $session = KasmUsageSession::where('kasm_workspace_id', $workspace->id)
                    ->whereNull('stopped_at')
                    ->first();

                if ($session) {
                    $session->update([
                        'stopped_at' => now(),
                        'duration_seconds' => now()->diffInSeconds($session->started_at),
                    ]);

                    // Calculate billable duration (round to 15-min increments)
                    $minutes = ceil($session->duration_seconds / 60);
                    $billableMinutes = ceil($minutes / 15) * 15;
                    $billableSeconds = $billableMinutes * 60;

                    $session->update([
                        'duration_billable_seconds' => $billableSeconds,
                        'total_cost' => ($billableSeconds / 3600) * $session->cost_per_hour,
                    ]);
                }

                $workspace->update(['status' => 'stopped']);
            }
        }
    }
}

4. Monthly Billing

// app/Console/Commands/BillKasmUsage.php
// Run monthly on billing cycle date

class BillKasmUsage extends Command
{
    public function handle()
    {
        $users = User::has('kasmWorkspaces')->get();

        foreach ($users as $user) {
            // Get unbilled usage sessions
            $sessions = KasmUsageSession::whereHas('workspace', function ($q) use ($user) {
                $q->where('user_id', $user->id);
            })
            ->whereNull('invoice_id')
            ->where('stopped_at', '!=', null)
            ->get();

            if ($sessions->isEmpty()) continue;

            $totalCost = $sessions->sum('total_cost');

            // Create invoice
            $invoice = Invoice::create([
                'user_id' => $user->id,
                'total' => $totalCost,
                'currency' => 'USD',
                'due_date' => now()->addDays(7),
                'description' => 'Kasm Workspace Usage - ' . now()->format('F Y'),
            ]);

            // Add line items
            foreach ($sessions as $session) {
                InvoiceItem::create([
                    'invoice_id' => $invoice->id,
                    'description' => sprintf(
                        '%s - %s (%d minutes @ $%s/hour)',
                        $session->workspace->workspace_type,
                        $session->started_at->format('M d, Y'),
                        $session->duration_billable_seconds / 60,
                        number_format($session->cost_per_hour, 2)
                    ),
                    'amount' => $session->total_cost,
                    'quantity' => 1,
                ]);

                // Mark session as billed
                $session->update(['invoice_id' => $invoice->id]);
            }

            // Send invoice email
            Mail::to($user)->send(new InvoiceGenerated($invoice));

            // Charge payment method
            if ($user->hasDefaultPaymentMethod()) {
                try {
                    $payment = $user->charge($totalCost * 100, $user->defaultPaymentMethod());
                    $invoice->update(['paid_at' => now()]);
                } catch (\Exception $e) {
                    // Payment failed - send notification
                    Mail::to($user)->send(new PaymentFailed($invoice));
                }
            }
        }
    }
}

Customer Dashboard Features

Workspace Management

  • Start/Stop Controls: Customer can start/stop workspace from dashboard
  • Current Session Timer: "Running for 2 hours 37 minutes - $0.50"
  • Monthly Usage Summary: "This month: $47.35 (23.5 hours)"
  • Session History: List of all sessions with durations and costs
  • Workspace Access: One-click button to launch workspace in new tab

Running Cost Indicator

<!-- resources/js/Pages/Services/KasmWorkspace.vue -->
<template>
  <div class="workspace-status">
    <div v-if="workspace.status === 'running'" class="running">
      <Icon name="circle-dot" class="text-green-500 animate-pulse" />
      <span>Running for {{ runningDuration }}</span>
      <span class="text-lg font-bold">${{ currentCost }}</span>
      <button @click="stopWorkspace">Stop Workspace</button>
    </div>

    <div v-else class="stopped">
      <Icon name="circle" class="text-gray-400" />
      <span>Stopped</span>
      <button @click="startWorkspace">Start Workspace</button>
    </div>

    <div class="usage-summary">
      <h3>This Month's Usage</h3>
      <p class="text-2xl">${{ monthlyTotal }}</p>
      <p class="text-sm text-gray-600">{{ totalHours }} hours</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { router } from '@inertiajs/vue3'

const props = defineProps(['workspace'])
const runningDuration = ref('0m')
const currentCost = ref(0)

// Update every minute if running
let interval
onMounted(() => {
  if (props.workspace.status === 'running') {
    updateRunningCost()
    interval = setInterval(updateRunningCost, 60000) // Every minute
  }
})

function updateRunningCost() {
  const started = new Date(props.workspace.last_started_at)
  const now = new Date()
  const minutes = Math.floor((now - started) / 60000)
  const hours = Math.floor(minutes / 60)
  const mins = minutes % 60

  runningDuration.value = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`

  // Calculate cost (round to 15-min increments)
  const billableMinutes = Math.ceil(minutes / 15) * 15
  currentCost.value = ((billableMinutes / 60) * props.workspace.cost_per_hour).toFixed(2)
}

function startWorkspace() {
  router.post(`/kasm/${props.workspace.id}/start`)
}

function stopWorkspace() {
  router.post(`/kasm/${props.workspace.id}/stop`)
}
</script>

Part 2: Multi-Tenancy (White-Label Resellers)

What is Multi-Tenancy?

Allowing resellers to run their own branded billing platform using your infrastructure. Each reseller (tenant) has:

  • Their own customers
  • Their own branding (logo, colors, domain)
  • Their own pricing
  • Isolated database

Tenancy for Laravel Package

Package: https://tenancyforlaravel.com/ Documentation: https://tenancyforlaravel.com/docs/v3/

Key Features:

  • Automatic tenant identification (by domain, subdomain, or path)
  • Database per tenant (complete isolation)
  • Tenant-aware caching, filesystems, queues
  • Easy migrations across all tenants
  • Central app + tenant apps architecture

Architecture

Central Application

  • Domain: ezscale.cloud (your main application)
  • Purpose: Manage resellers (tenants), global settings, wholesale billing
  • Database: ezscale_central (stores tenant list, domains, configs)

Tenant Applications

  • Domains: reseller1.com, reseller2.hosting, custom-domain.net
  • Purpose: Each reseller's branded billing platform
  • Databases: tenant_reseller1, tenant_reseller2, tenant_custom

Database Structure

Central Database (ezscale_central)

tenants table:
├── id
├── name (Reseller Company Name)
├── slug (reseller1, reseller2)
├── database_name (tenant_reseller1)
├── domain (reseller1.com, custom-domain.net)
├── status (active, suspended, trial)
├── owner_email
├── owner_name
├── created_at
├── trial_ends_at (nullable)
├── suspended_at (nullable)

tenant_domains table:
├── id
├── tenant_id
├── domain (reseller1.com, reseller1.ezscale.cloud, custom-domain.net)
├── type (primary, alias)
├── ssl_status (pending, active, failed)
├── verified_at
├── created_at

tenant_billing table:
├── id
├── tenant_id
├── plan_id (reseller tier: basic, pro, enterprise)
├── wholesale_discount_percent (e.g., 30% off retail prices)
├── monthly_fee (platform fee - e.g., $99/month)
├── commission_percent (if commission-based instead of wholesale)
├── billing_cycle_day (1-28)
├── next_billing_date
├── status (active, past_due, cancelled)

tenant_branding table:
├── id
├── tenant_id
├── logo_url
├── favicon_url
├── primary_color (#3B82F6)
├── secondary_color
├── company_name
├── support_email
├── support_phone
├── from_email (noreply@reseller.com)
├── from_name (Reseller Hosting)
├── custom_css (nullable - advanced branding)

Tenant Databases (e.g., tenant_reseller1)

Each tenant gets a complete copy of the main application schema:

  • users (reseller's customers)
  • plans (reseller's custom pricing)
  • subscriptions
  • invoices
  • services
  • kasm_workspaces
  • ... all other tables from main schema

Key difference: Tenant plans reference wholesale prices in central database, but show custom prices to customers.

Installation & Setup

# Install Tenancy for Laravel
composer require stancl/tenancy

# Publish config and migrations
php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider'

# Run central migrations
php artisan migrate

# Configure tenancy
# config/tenancy.php
return [
    'tenant_model' => \App\Models\Tenant::class,
    'id_generator' => \Stancl\Tenancy\UUIDGenerator::class,

    'database' => [
        'prefix' => 'tenant_',
        'template_tenant_connection' => 'mysql',
    ],

    'bootstrappers' => [
        \Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class,
        \Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
        \Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
        \Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
    ],
];

Creating a Reseller (Tenant)

// app/Http/Controllers/Admin/ResellerController.php
public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string',
        'email' => 'required|email|unique:tenants,owner_email',
        'domain' => 'required|string|unique:tenant_domains,domain',
    ]);

    // Create tenant
    $tenant = Tenant::create([
        'name' => $validated['name'],
        'slug' => Str::slug($validated['name']),
        'owner_email' => $validated['email'],
    ]);

    // Create domain
    $tenant->domains()->create([
        'domain' => $validated['domain'],
        'type' => 'primary',
    ]);

    // Run migrations for tenant database
    $tenant->run(function () {
        Artisan::call('migrate', ['--database' => 'tenant', '--force' => true]);

        // Seed initial data (plans, roles, etc.)
        Artisan::call('db:seed', ['--class' => 'TenantSeeder']);
    });

    // Set up branding defaults
    TenantBranding::create([
        'tenant_id' => $tenant->id,
        'company_name' => $validated['name'],
        'primary_color' => '#3B82F6',
        'support_email' => $validated['email'],
    ]);

    // Send welcome email to reseller
    Mail::to($validated['email'])->send(new ResellerWelcome($tenant));

    return redirect()->route('admin.resellers.index')
        ->with('success', 'Reseller created successfully!');
}

Tenant Identification (Automatic)

// routes/web.php (Central app routes)
Route::get('/', [HomeController::class, 'index']);
Route::get('/admin/resellers', [Admin\ResellerController::class, 'index'])
    ->middleware(['auth', 'admin']);

// routes/tenant.php (Tenant-specific routes)
Route::middleware('tenant')->group(function () {
    // These routes run within tenant context
    Route::get('/', [Tenant\DashboardController::class, 'index']);
    Route::get('/services', [Tenant\ServiceController::class, 'index']);
    Route::post('/kasm/order', [Tenant\KasmController::class, 'createOrder']);
    // ... all customer-facing routes
});

// Middleware automatically identifies tenant by domain
// If request is to reseller1.com → loads tenant_reseller1 database
// If request is to ezscale.cloud → uses central database

Reseller Pricing Control

// Tenant database: plans table
// Reseller can set any price they want

CREATE TABLE plans (
    id INT PRIMARY KEY,
    name VARCHAR(255),
    description TEXT,
    price DECIMAL(10, 2), -- Reseller's customer price
    wholesale_price DECIMAL(10, 2), -- What reseller pays EZSCALE
    service_type VARCHAR(50),
    -- ... other fields
);

// Example:
// VPS Basic
// - Wholesale price (you charge reseller): $8/month
// - Reseller's price (they charge customer): $15/month
// - Reseller's profit: $7/month per customer

// When customer subscribes:
// 1. Customer pays reseller $15
// 2. Reseller pays you $8 (via wholesale invoice)
// 3. Reseller keeps $7 profit

Wholesale Billing (Billing Resellers)

// app/Console/Commands/BillResellers.php
// Run monthly to bill resellers for their customer usage

class BillResellers extends Command
{
    public function handle()
    {
        $tenants = Tenant::where('status', 'active')->get();

        foreach ($tenants as $tenant) {
            // Switch to tenant database
            $tenant->run(function () use ($tenant) {
                // Count active subscriptions
                $subscriptions = Subscription::where('status', 'active')->get();

                $wholesaleTotal = 0;

                foreach ($subscriptions as $subscription) {
                    $plan = Plan::find($subscription->plan_id);
                    $wholesaleTotal += $plan->wholesale_price;
                }

                // Add platform fee
                $platformFee = TenantBilling::where('tenant_id', $tenant->id)
                    ->value('monthly_fee');

                $totalDue = $wholesaleTotal + $platformFee;

                // Create wholesale invoice in CENTRAL database
                tenancy()->end(); // Switch back to central

                WholesaleInvoice::create([
                    'tenant_id' => $tenant->id,
                    'platform_fee' => $platformFee,
                    'usage_charges' => $wholesaleTotal,
                    'total' => $totalDue,
                    'due_date' => now()->addDays(7),
                ]);

                // Email reseller
                Mail::to($tenant->owner_email)->send(
                    new ResellerInvoice($tenant, $totalDue)
                );
            });
        }
    }
}

Reseller Dashboard (Central App)

Resellers log into ezscale.cloud/reseller to:

  • View their wholesale invoices
  • See customer count and revenue
  • Manage branding (logo, colors)
  • View usage statistics
  • Configure pricing for their plans
  • Add/manage their custom domains

Branding Customization

// app/Http/Controllers/Reseller/BrandingController.php
public function update(Request $request)
{
    $tenant = Auth::user()->tenant;

    $validated = $request->validate([
        'logo' => 'nullable|image|max:2048',
        'primary_color' => 'required|string',
        'company_name' => 'required|string',
        'support_email' => 'required|email',
    ]);

    if ($request->hasFile('logo')) {
        $logoPath = $request->file('logo')->store('tenant_logos', 's3');
        $validated['logo_url'] = Storage::disk('s3')->url($logoPath);
    }

    $tenant->branding()->update($validated);

    return back()->with('success', 'Branding updated!');
}

Loading Tenant Branding

// app/Http/Middleware/InjectTenantBranding.php
public function handle($request, $next)
{
    if (tenancy()->initialized) {
        $branding = TenantBranding::where('tenant_id', tenant('id'))->first();

        // Share branding with all views
        View::share('branding', $branding);

        // Inject CSS variables
        if ($branding) {
            $customCss = "
                :root {
                    --primary-color: {$branding->primary_color};
                    --secondary-color: {$branding->secondary_color};
                }
            ";
            View::share('customCss', $customCss);
        }
    }

    return $next($request);
}
<!-- resources/views/layouts/tenant.blade.php -->
<!DOCTYPE html>
<html>
<head>
    <title>{{ $branding->company_name ?? 'EZSCALE Hosting' }}</title>
    <link rel="icon" href="{{ $branding->favicon_url ?? asset('favicon.ico') }}">

    @if(isset($customCss))
        <style>{!! $customCss !!}</style>
    @endif

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
    <nav>
        <img src="{{ $branding->logo_url ?? asset('logo.png') }}" alt="Logo">
        <span>{{ $branding->company_name }}</span>
    </nav>

    @yield('content')

    <footer>
        <p>Support: {{ $branding->support_email }}</p>
    </footer>
</body>
</html>

Integration Points

Kasm + Multi-Tenancy

  • Each tenant (reseller) can offer Kasm workspaces
  • Resellers set their own Kasm pricing
  • Usage tracked per tenant database
  • Wholesale billing aggregates all tenant usage

Database Structure

Central DB (ezscale_central)
├── tenants
├── tenant_domains
├── tenant_billing
└── wholesale_invoices

Tenant DB (tenant_reseller1)
├── users (reseller's customers)
├── subscriptions
├── kasm_workspaces
├── kasm_usage_sessions
└── invoices (customer invoices)

Implementation Phases

Phase 12: Kasm Workspaces (New Phase)

  • Research Kasm licensing (per named user vs concurrent session)
  • Set up Kasm Workspaces instance
  • Create Kasm API integration service
  • Build workspace provisioning automation
  • Implement usage tracking (15-min increments)
  • Build workspace management UI
  • Implement start/stop controls
  • Create workspace templates (dev + business)
  • Build monthly usage billing
  • Test end-to-end workflow

Phase 13: Multi-Tenancy (New Phase)

  • Install Tenancy for Laravel package
  • Configure tenant identification (domain-based)
  • Create central reseller management UI
  • Build tenant creation workflow
  • Implement automatic tenant migrations
  • Build branding customization system
  • Create wholesale billing system
  • Build reseller dashboard
  • Test multi-domain SSL (Let's Encrypt)
  • Test tenant isolation thoroughly
  • Create reseller onboarding documentation

Open Questions

  • Kasm Licensing: Which Kasm license tier (per named user vs concurrent)? Cost per user?
  • Kasm Infrastructure: Self-hosted Kasm server or Kasm cloud? If self-hosted, hardware requirements?
  • Reseller Trials: Should resellers get trial period? How long?
  • Reseller Pricing: Fixed platform fee ($99/month) or percentage of revenue?
  • Minimum Customers: Require minimum customer count before reseller can launch?
  • Support Responsibility: Who handles tenant customer support - you or reseller?

Summary

This document provides comprehensive implementation plans for:

  1. Kasm Workspaces Integration

    • Fully automated provisioning
    • Hourly billing with 15-minute increments
    • Real-time usage tracking
    • Developer and Business workspace types
    • Complete API integration
  2. Multi-Tenancy for Resellers

    • Database-per-tenant isolation
    • Full white-label branding
    • Custom domain support
    • Reseller pricing control
    • Wholesale billing system

Both features significantly expand EZSCALE's service offerings and create new revenue streams through workspace hosting and reseller partnerships.

References: