Multi-Tenancy¶
Learn how to use the Laravel Evolution API package in multi-tenant applications.
Overview¶
The package supports multiple Evolution API connections, making it ideal for:
- SaaS Applications - Each tenant has their own WhatsApp instance
- Agency Setups - Managing multiple client accounts
- Distributed Systems - Multiple Evolution API servers
Configuration¶
Multiple Connections¶
Define multiple Evolution API connections:
// config/evolution-api.php
'connections' => [
'default' => [
'server_url' => env('EVOLUTION_API_URL', 'http://localhost:8080'),
'api_key' => env('EVOLUTION_API_KEY'),
],
'tenant_a' => [
'server_url' => env('EVOLUTION_API_URL_TENANT_A'),
'api_key' => env('EVOLUTION_API_KEY_TENANT_A'),
],
'tenant_b' => [
'server_url' => env('EVOLUTION_API_URL_TENANT_B'),
'api_key' => env('EVOLUTION_API_KEY_TENANT_B'),
],
],
Environment Variables¶
# .env
EVOLUTION_API_URL=http://localhost:8080
EVOLUTION_API_KEY=your-global-key
EVOLUTION_API_URL_TENANT_A=http://tenant-a-api:8080
EVOLUTION_API_KEY_TENANT_A=tenant-a-key
EVOLUTION_API_URL_TENANT_B=http://tenant-b-api:8080
EVOLUTION_API_KEY_TENANT_B=tenant-b-key
Using Connections¶
Switching Connections¶
use Lynkbyte\EvolutionApi\Facades\EvolutionApi;
// Use default connection
$instances = EvolutionApi::instances()->list();
// Switch to tenant connection
$instances = EvolutionApi::connection('tenant_a')
->instances()
->list();
// Chain with instance
$messages = EvolutionApi::connection('tenant_a')
->for('my-instance')
->messages()
->sendText('5511999999999', 'Hello!');
Connection in Jobs¶
use Lynkbyte\EvolutionApi\Jobs\SendMessageJob;
// Send via specific connection
SendMessageJob::text(
instanceName: 'my-instance',
number: '5511999999999',
text: 'Hello from tenant!',
connectionName: 'tenant_a'
)->dispatch();
Dynamic Connections¶
Runtime Configuration¶
Create connections dynamically:
use Lynkbyte\EvolutionApi\EvolutionClient;
// Create client with custom config
$client = new EvolutionClient(
serverUrl: $tenant->evolution_url,
apiKey: $tenant->evolution_api_key
);
// Use the client
$instances = $client->instances()->list();
Service Provider Registration¶
// app/Providers/EvolutionServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Lynkbyte\EvolutionApi\EvolutionClient;
class EvolutionServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Dynamically register connections for each tenant
$tenants = Tenant::whereNotNull('evolution_url')->get();
foreach ($tenants as $tenant) {
config([
"evolution-api.connections.{$tenant->id}" => [
'server_url' => $tenant->evolution_url,
'api_key' => $tenant->evolution_api_key,
],
]);
}
}
}
Per-Request Context¶
Middleware Approach¶
Set connection based on current tenant:
// app/Http/Middleware/SetEvolutionConnection.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Lynkbyte\EvolutionApi\Facades\EvolutionApi;
class SetEvolutionConnection
{
public function handle(Request $request, Closure $next)
{
$tenant = $request->user()?->tenant;
if ($tenant && $tenant->evolution_connection) {
EvolutionApi::connection($tenant->evolution_connection);
}
return $next($request);
}
}
Service Container Binding¶
// app/Providers/AppServiceProvider.php
use Lynkbyte\EvolutionApi\EvolutionClient;
public function register(): void
{
$this->app->singleton(EvolutionClient::class, function ($app) {
$tenant = $app['request']->user()?->tenant;
if ($tenant) {
return new EvolutionClient(
serverUrl: $tenant->evolution_url,
apiKey: $tenant->evolution_api_key
);
}
return new EvolutionClient(
serverUrl: config('evolution-api.server_url'),
apiKey: config('evolution-api.api_key')
);
});
}
Webhook Handling¶
Tenant Identification¶
Identify tenant from webhook:
// app/Http/Controllers/TenantWebhookController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Lynkbyte\EvolutionApi\Webhooks\WebhookProcessor;
class TenantWebhookController extends Controller
{
public function handle(Request $request, string $tenantId)
{
$tenant = Tenant::findOrFail($tenantId);
// Set tenant context
tenancy()->initialize($tenant);
// Process webhook
$processor = app(WebhookProcessor::class);
$processor->process($request->all());
return response()->json(['status' => 'success']);
}
}
Per-Tenant Webhook URLs¶
Configure unique webhook URLs per tenant:
// routes/api.php
Route::post('/webhook/{tenant}', [TenantWebhookController::class, 'handle'])
->name('evolution.webhook.tenant');
// When creating instance for tenant
$evolution->connection($tenant->id)
->for($instanceName)
->webhooks()
->configure([
'url' => route('evolution.webhook.tenant', $tenant->id),
'events' => ['MESSAGES_UPSERT', 'CONNECTION_UPDATE'],
]);
Database Scoping¶
Tenant-Scoped Models¶
If storing messages/webhooks in database:
// app/Models/Message.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Scopes\TenantScope;
class Message extends Model
{
protected static function booted(): void
{
static::addGlobalScope(new TenantScope);
static::creating(function ($model) {
$model->tenant_id = tenant()->id;
});
}
}
Migration with Tenant Column¶
Schema::create('evolution_messages', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->onDelete('cascade');
$table->string('instance_name');
$table->string('message_id');
// ...
$table->index(['tenant_id', 'instance_name']);
});
Integration with Popular Packages¶
Spatie Laravel-Multitenancy¶
use Spatie\Multitenancy\Tasks\SwitchTenantTask;
use Lynkbyte\EvolutionApi\Facades\EvolutionApi;
class SwitchEvolutionConnectionTask implements SwitchTenantTask
{
public function makeCurrent(Tenant $tenant): void
{
if ($tenant->evolution_connection) {
EvolutionApi::connection($tenant->evolution_connection);
}
}
public function forgetCurrent(): void
{
EvolutionApi::connection('default');
}
}
Stancl/Tenancy¶
// config/tenancy.php
'bootstrappers' => [
// ...
App\Tenancy\EvolutionApiBootstrapper::class,
],
// app/Tenancy/EvolutionApiBootstrapper.php
namespace App\Tenancy;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Lynkbyte\EvolutionApi\Facades\EvolutionApi;
class EvolutionApiBootstrapper implements TenancyBootstrapper
{
public function bootstrap(Tenant $tenant): void
{
if ($tenant->evolution_connection) {
EvolutionApi::connection($tenant->evolution_connection);
}
}
public function revert(): void
{
EvolutionApi::connection('default');
}
}
Complete Multi-Tenant Example¶
Tenant Model¶
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tenant extends Model
{
protected $fillable = [
'name',
'evolution_url',
'evolution_api_key',
'evolution_instance',
];
protected $casts = [
'evolution_api_key' => 'encrypted',
];
public function getEvolutionClient(): \Lynkbyte\EvolutionApi\EvolutionClient
{
return new \Lynkbyte\EvolutionApi\EvolutionClient(
serverUrl: $this->evolution_url,
apiKey: $this->evolution_api_key
);
}
}
Tenant WhatsApp Service¶
namespace App\Services;
use App\Models\Tenant;
use Lynkbyte\EvolutionApi\EvolutionClient;
use Lynkbyte\EvolutionApi\Jobs\SendMessageJob;
class TenantWhatsAppService
{
protected EvolutionClient $client;
protected Tenant $tenant;
public function __construct(Tenant $tenant)
{
$this->tenant = $tenant;
$this->client = $tenant->getEvolutionClient();
}
public function sendMessage(string $number, string $text): void
{
// Queue message with tenant's connection
SendMessageJob::text(
instanceName: $this->tenant->evolution_instance,
number: $number,
text: $text,
connectionName: "tenant_{$this->tenant->id}"
)->dispatch();
}
public function getStatus(): array
{
return $this->client
->for($this->tenant->evolution_instance)
->instances()
->getStatus();
}
public function getQrCode(): ?string
{
$response = $this->client
->for($this->tenant->evolution_instance)
->instances()
->connect();
return $response->getData()['qrcode']['base64'] ?? null;
}
}
Usage in Controller¶
namespace App\Http\Controllers;
use App\Services\TenantWhatsAppService;
use Illuminate\Http\Request;
class WhatsAppController extends Controller
{
public function sendMessage(Request $request)
{
$tenant = $request->user()->tenant;
$service = new TenantWhatsAppService($tenant);
$service->sendMessage(
number: $request->input('number'),
text: $request->input('message')
);
return response()->json(['status' => 'queued']);
}
public function status(Request $request)
{
$tenant = $request->user()->tenant;
$service = new TenantWhatsAppService($tenant);
return response()->json($service->getStatus());
}
}
Best Practices¶
1. Isolate Tenant Data¶
// Always scope queries to tenant
Message::where('tenant_id', $tenant->id)->get();
// Use global scopes
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('tenant_id', tenant()->id);
}
}
2. Cache Connection Configs¶
// Cache tenant connection config
$config = Cache::remember("tenant_{$tenant->id}_evolution", 3600, function () use ($tenant) {
return [
'server_url' => $tenant->evolution_url,
'api_key' => $tenant->evolution_api_key,
];
});
3. Handle Connection Failures¶
try {
$client = $tenant->getEvolutionClient();
$status = $client->for($instance)->instances()->getStatus();
} catch (ConnectionException $e) {
Log::error("Tenant {$tenant->id} Evolution API unreachable", [
'url' => $tenant->evolution_url,
'error' => $e->getMessage(),
]);
// Mark tenant's WhatsApp as unavailable
$tenant->update(['whatsapp_available' => false]);
}