# 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 ```http 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: ` and `X-API-Secret: ` - Keys generated in Kasm admin panel ### Database Schema ```sql 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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 ```vue ``` --- ## 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`) ```sql 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 ```bash # 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) ```php // 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) ```php // 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 ```php // 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) ```php // 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 ```php // 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 ```php // 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); } ``` ```blade {{ $branding->company_name ?? 'EZSCALE Hosting' }} @if(isset($customCss)) @endif @vite(['resources/css/app.css', 'resources/js/app.js']) @yield('content') ``` --- ## 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**: - [Kasm Workspaces Documentation](https://docs.kasm.com/) - [Kasm Developer API](https://docs.kasm.com/docs/latest/developers/developer_api/) - [Tenancy for Laravel](https://tenancyforlaravel.com/) - [Multi-Database Tenancy Docs](https://tenancyforlaravel.com/docs/v3/multi-database-tenancy/)