Multi-Tenancy
How Dispatch scopes jobs and workflows to app and organization contexts.
Multi-tenancy support is built into Dispatch via the scope package and the ScopeAppID / ScopeOrgID fields on every entity. It integrates naturally with the Forge framework but degrades gracefully in standalone mode.
Scope fields on entities
Every major entity carries scope fields:
| Entity | Fields |
|---|---|
job.Job | ScopeAppID, ScopeOrgID |
workflow.Run | ScopeAppID, ScopeOrgID |
cron.Entry | ScopeAppID, ScopeOrgID |
dlq.Entry | ScopeAppID, ScopeOrgID |
These fields are populated automatically from the context at enqueue time via scope.Capture(ctx).
The scope middleware
Add middleware.Scope() to the engine's middleware chain. It reads scope fields from the enqueued Job and injects them back into the handler's context.Context:
eng := engine.Build(d,
engine.WithMiddleware(middleware.Scope()),
)Inside a job handler, retrieve the scope:
import "github.com/xraph/dispatch/scope"
func(ctx context.Context, input MyInput) error {
appID, orgID := scope.Capture(ctx)
// use appID / orgID to scope database queries
return nil
}The scope package
In standalone mode, scope.Capture returns empty strings (no-op):
import "github.com/xraph/dispatch/scope"
appID, orgID := scope.Capture(ctx) // "" in standalone mode
ctx = scope.Restore(ctx, appID, orgID) // inject into contextWhen running inside the Forge framework, the scope package reads app and organization IDs from the Forge context automatically.
Per-queue tenant routing
Combine scope with per-queue configuration to isolate tenant traffic:
eng := engine.Build(d,
engine.WithQueueConfig(
queue.Config{Name: "tenant-acme", MaxConcurrency: 5},
queue.Config{Name: "tenant-beta", MaxConcurrency: 2},
),
)
// Enqueue to a tenant-specific queue:
engine.EnqueueOnQueue(ctx, eng, SendEmail, input, "tenant-acme")