← Tenancy

TENANCY FEATURE

Queue Tenant Propagation

Tenant context travels with queued jobs — _tenant key injected automatically.

Tenant context travels with queued jobs — no manual propagation.

Producer (request context)

// Dispatch while tenant = 'acme'
$serializer->wrap([
    'action' => 'generate_report',
    'product_id' => 42,
]);
// ↓ _tenant injected automatically
→ queue →

Serialized payload

{
    "action": "generate_report",
    "product_id": 42,
    "_tenant": {
        "org": "acme",
        "locale": "en",
        "theme": "acme-theme",
        "env": "production"
    }
}
→ worker →

Worker side

1
Worker receives message
Queue consumer dequeues job payload
2
TenantAwareJobSerializer::unwrap()
Extracts _tenant key, strips it from clean payload
3
TenantContext::set($context)
Context restored — ORM, templates, queues all scoped
4
Handler runs with tenant scope
Queries hit tenant_id = 'acme' automatically

Wrap and unwrap API

$serializer = new TenantAwareJobSerializer();

// Producer:
$payload = $serializer->wrap(['action' => 'generate_report', 'product_id' => 42]);
// → ['action' => ..., '_tenant' => ['org' => 'acme', 'locale' => 'en', ...]]

// Worker:
[$cleanPayload, $tenantContext] = $serializer->unwrap($queueMessage);
TenantContext::set($tenantContext);
// ORM, templates, everything scoped to 'acme' now

Design decision: the default tenant (no active context) is NOT propagated — only explicit tenant contexts travel.

Why this matters for SaaS

Without tenant propagation, a queued job runs in the wrong scope — a report job for Acme reads Globex's data. Every job needs manual tenant_id plumbing.

With TenantAwareJobSerializer, tenant context is an invisible envelope on every job. Workers restore it transparently. Zero plumbing in application code.