Init Commit
This commit is contained in:
923
KASM_AND_MULTITENANCY.md
Normal file
923
KASM_AND_MULTITENANCY.md
Normal file
@@ -0,0 +1,923 @@
|
||||
# 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/)
|
||||
Reference in New Issue
Block a user