Dispatch

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:

EntityFields
job.JobScopeAppID, ScopeOrgID
workflow.RunScopeAppID, ScopeOrgID
cron.EntryScopeAppID, ScopeOrgID
dlq.EntryScopeAppID, 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 context

When 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")

On this page