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 entities01
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 middleware02
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 package03
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 routing04
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")