Files
website/KASM_AND_MULTITENANCY.md
Claude Dev cf7669f270 Updated
2026-02-09 01:49:02 -05:00

928 lines
28 KiB
Markdown

# 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: <key>` and `X-API-Secret: <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
<!-- 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`)
```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
All commands below should be run from the `website/` directory:
```bash
cd website
# 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
<!-- 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**:
- [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/)