924 lines
28 KiB
Markdown
924 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
|
|
|
|
```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
|
|
<!-- 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/)
|