The browser knows which customer is working. The queue knows that something needs doing. A reliable SaaS application has to preserve the connection between those two facts.
The request ends before the responsibility does
Imagine a customer uploading a product image. The application accepts it, schedules a thumbnail, and returns a success response. A worker will finish the expensive part later. To the customer, this is one action: “prepare my image.” To the software, it is already two executions.
The second execution may start after the browser tab has closed. It may run in another process. The hostname that identified the customer is gone, along with the request that carried it. Yet the thumbnail still belongs to the same organization, under the same business rules.
This is an illustrative scenario, but the architectural boundary is real. Moving work into a queue also moves responsibility for its context. A job can arrive intact and still arrive without enough information to run correctly.
A perfectly delivered message can be incomplete
A message containing an asset ID and a transformation name tells a worker what to process. Depending on the application, it may leave other questions unanswered: which organization’s configuration should apply, which locale should an accompanying notification use, and which environment does this work belong to?
Those answers might be recoverable from the asset. That can be a valid design. The fragile version is the one where each downstream service guesses differently, or reads whatever “current tenant” happens to be left in memory. Local testing with one customer rarely challenges that assumption. A shared worker serving many customers will.
Adding a tenant field to a database table addresses one part of the design. Preserving the execution context across a queue is another. Semitexa makes that handoff explicit with a shared tenant context and a serializer at the message boundary.
Make the context part of the handoff
TenantAwareJobSerializer::wrap() reads the current context from TenantContextStore. For a concrete, non-default tenant, it adds an envelope under _tenant. The receiving side can extract that envelope and rebuild the context before application work begins.
- Resolve: establish the tenant for the incoming execution.
- Attach: serialize the supported context into the message.
- Restore: recover that context at the worker boundary.
- Execute: run the operation with an explicit tenant context available.
- Clear: end the context’s lifetime when the execution finishes.
The queue separates two executions. The envelope connects their tenant context; the execution lifecycle owns cleanup.
An illustrative wrapped payload looks like this. The application fields are examples; _tenant and its fields follow the serializer’s current format:
{
"assetId": "image-42",
"variantKey": "thumbnail",
"_tenant": {
"tenantId": "acme",
"strategy": "subdomain",
"locale": "en",
"environment": "prod"
}
}
The implementation serializes the organization ID and resolution strategy, plus locale and environment when those layers are explicitly present. It does not serialize every possible context layer. A custom layer needs an intentional propagation design of its own.
There is also a deliberate default case: when no context exists, or the organization is the default tenant, wrapping leaves the payload unchanged. Consumers therefore need a policy for messages without _tenant. A task that requires an organization should reject missing tenant identity at its boundary.
This already has a job in Semitexa
The media package provides a concrete example. MediaQueueDispatcher wraps a serialized transformation message before sending it. On the receiving side, MediaWorker::processPayload() calls unwrapAndRestore() before reconstructing the media message and processing it.
Unwrapping removes the envelope from the application payload. Restoring places the recovered context into the store’s CLI fallback. The transformation message keeps its own domain shape, while services that consult the context store have a tenant available during execution.
The media transformation path also passes the asset’s tenant ID explicitly when resolving collection policy and generating a variant. That detail matters: the queued context and the persisted asset identity play distinct roles. Application code still has to enforce the relationship between them wherever that relationship controls access or storage.
This is the concrete problem Semitexa addresses: the HTTP request can disappear without making an integrated background operation reconstruct tenant context from ambient process state. The producer and consumer share one envelope format and one restoration mechanism. Custom queue paths must wire those mechanisms into their own execution boundary.
A worker must also know when to forget
Now imagine the same worker processes a second message. It has no tenant envelope. If the first job’s context remains in memory, the second job may observe the first customer’s identity. Correct propagation into one job has become incorrect propagation into the next.
This is where tenancy meets the long-running PHP lifecycle. In a Swoole coroutine, Semitexa’s tenant store reads coroutine-local state. Outside a coroutine, it uses a process fallback. Those storage choices support different execution environments and require a clear end to each unit of work.
TenantContextStore registers cleanup with PerRequestStateRegistry when context is set. Its cleanup callback clears the context of the current execution: the coroutine-local slot inside a coroutine, the process fallback outside one. The surrounding runtime must actually invoke that lifecycle at the appropriate boundary.
The serializer alone does not provide that boundary. In particular, unwrapAndRestore() sets the fallback when an envelope exists; a message without an envelope does not make that method clear an earlier fallback. A custom worker loop must explicitly ensure cleanup, including on exceptions and early returns. A coroutine consumer must establish context in the coroutine’s own store rather than assuming the CLI fallback is visible there.
Preserve the tenant across the handoff. End its lifetime after the work. Both steps belong in the design.
Put each guarantee where it can be enforced
Once the context travels reliably, the rest of the application has a stable input. It still needs to use that input correctly.
- Resolution determines which organization an execution refers to.
- Authorization determines whether the actor or job may perform the action for that organization.
- Data isolation scopes queries, cache keys, storage paths, and other resources where the application accesses them.
- Propagation and cleanup preserve context across handoffs and prevent it from surviving into unrelated work.
The envelope carries identity; it does not prove permission. Queue ingress must be trusted or validated, tenant-owned records must be checked, and tenant-sensitive operations need an explicit policy for absent context. Semitexa supplies the context mechanisms. An application earns its isolation guarantee through the boundaries that consume them.
Test the handoff with two customers
A useful test goes beyond proving that one message round-trips through a serializer. Process work for tenant A, then tenant B, in the same worker. Follow both with a message that has no tenant envelope. Check what each operation can observe. Repeat the sequence with a failure during A’s work, so cleanup has to survive an exception.
At the serialization level, Semitexa’s tenancy package includes dedicated propagation tests. At the application level, test the effects: which configuration was selected, which records were accessed, and where the output was stored. Also check any locale or environment values the workflow promises to preserve. These are acceptance criteria for an integration, not a claim that a serializer test proves every application safe.
The larger lesson is useful well beyond media processing. Exports, notifications, and scheduled work all cross moments where the original request is no longer available. Making tenant context explicit turns those moments into boundaries that can be inspected and tested. A customer’s action can then continue after the response, with its ownership still accounted for.
Follow the lifetime of the work.
Explore tenant propagation in the Framework guide, then see why execution boundaries matter in a process that stays alive.
This article was first published on the Semitexa blog, where the live demos run next to the text. Semitexa is on GitHub.
Top comments (0)