abdulsalam / laravel-context-flow
Distributed context propagation, correlation and trust-aware boundary policies on top of Laravel Context.
Package info
github.com/abdulsalamalkhatib96/laravel-context-flow
pkg:composer/abdulsalam/laravel-context-flow
Requires
- php: ^8.2
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/events: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/log: ^12.0|^13.0
- illuminate/queue: ^12.0|^13.0
- illuminate/routing: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- psr/http-message: ^1.1|^2.0
Requires (Dev)
- guzzlehttp/psr7: ^2.7
- laravel/framework: ^12.0|^13.0
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Distributed context propagation for Laravel applications, built on top of Laravel Context instead of replacing it.
The package preserves one logical workflow across HTTP requests, queued jobs, scheduled work and service-to-service HTTP calls without passing correlation or tenant metadata through dozens of constructors.
POST /withdraw CreateWithdrawalJob payment-api WebhookJob
C1 / E1 -> C1 / E2 -> C1 / E3 -> C1 / E4
caused by E1 caused by E2 caused by E3
Design rules
Illuminate\Support\Facades\Contextis the source of truth.correlation_ididentifies the complete workflow.execution_ididentifies one execution boundary.causation_idpoints to the execution that caused the current one.originremains the root boundary that started the workflow;boundaryis the current execution boundary.request_idis local to one HTTP request and is never reused by downstream services.- Network propagation is explicit and trust-aware. Application metadata is never sent just because it exists in
Context. - Context is metadata, not authorization. Never authorize a tenant, role or user from propagated context alone.
- The package does not generate or overwrite
traceparent/tracestate. OpenTelemetry instrumentation remains the owner of distributed tracing.
Requirements
- PHP 8.2+
- Laravel 12 or 13
Laravel 13 itself requires PHP 8.3+.
Installation
composer require abdulsalam/laravel-context-flow
Publish the configuration when you need custom keys or trust policies:
php artisan vendor:publish --tag=context-flow-config
The package auto-registers its service provider through Laravel package discovery.
Zero-config behavior
With the default configuration the package automatically:
- creates a correlation ID, execution ID and request ID for incoming HTTP requests;
- exposes the correlation and request IDs on HTTP responses;
- adds those IDs to Laravel Context, therefore Laravel logging receives them naturally;
- uses Laravel's native Context dehydration / hydration for queues;
- rotates the execution ID when a queued job starts and stores the previous execution as the causation ID;
- creates root contexts for Artisan commands and scheduled callbacks;
- adds queue metadata such as job ID, class, queue, connection and attempt;
- blocks business context from external HTTP destinations by default.
No HasContext job trait and no custom queue payload format are used.
Basic usage
Use Laravel Context normally:
use Illuminate\Support\Facades\Context; Context::add('tenant_id', $tenant->id); Context::add('actor_id', auth()->id()); Context::add('locale', app()->getLocale()); CreateWithdrawalJob::dispatch($withdrawal);
Inside the job:
Context::get('tenant_id'); Context::get('actor_id');
The package's own facade is intentionally small:
use Abdulsalam\LaravelContextFlow\Facades\ContextFlow; ContextFlow::correlationId(); ContextFlow::executionId(); ContextFlow::causationId(); ContextFlow::requestId();
It does not provide put/get/forget wrappers because Laravel Context already does that.
Registering application context keys
Only registered application keys are eligible for package-managed network propagation:
// config/context-flow.php 'keys' => [ 'tenant_id' => [ 'targets' => ['queue', 'http.internal'], 'accept_from' => ['internal'], 'priority' => 500, 'max_bytes' => 128, ], 'actor_id' => [ 'targets' => ['queue', 'http.internal'], 'accept_from' => ['internal'], 'priority' => 500, 'max_bytes' => 128, ], 'locale' => [ 'targets' => ['queue', 'http.internal'], 'accept_from' => ['internal', 'public'], 'priority' => 200, 'max_bytes' => 32, ], ],
Available targets:
queue
http.internal
http.partner
http.external
process
message_bus
process and message_bus are extension targets in the policy model; the built-in v1 runtime automatically integrates HTTP and Laravel queues.
Internal HTTP propagation
Declare internal destinations:
'http' => [ 'trusted_hosts' => [ '*.internal.example.com', '*.svc.cluster.local', ], ],
Then ordinary Laravel HTTP calls are enriched automatically:
Http::post('https://wallet.internal.example.com/withdrawals', [ 'amount' => 100, ]);
Typical headers:
X-Correlation-ID: 0199... X-Causation-ID: 0199... baggage: actor_id=99,tenant_id=71
X-Causation-ID is the caller's current execution_id. The receiving service creates its own execution and request IDs.
External HTTP is default-deny
By default:
Http::post('https://api.stripe.com/...');
receives no Context Flow headers.
This is controlled by:
'security' => [ 'default_remote_policy' => 'deny', ],
If you deliberately switch that policy away from deny, only keys explicitly targeting http.external become eligible. Do not put credentials, session material or PII in propagated context.
Signed internal context
A host allowlist controls what this service considers an internal destination. It does not prove that an inbound request really came from an internal service.
For inbound restricted baggage (tenant_id, actor_id, etc.), enable signing on all cooperating services:
CONTEXT_FLOW_SIGN_INTERNAL=true CONTEXT_FLOW_SIGNING_KEY="use-at-least-32-random-bytes-here"
Outgoing internal calls receive:
X-Context-Timestamp: 1789124211 X-Context-Signature: sha256=...
The signature covers the correlation ID, causation ID, canonical baggage and timestamp. Inbound requests are treated as internal only when the signature is valid and within the configured timestamp tolerance.
Important: a valid signature still does not turn context into an authorization source. Re-resolve the authenticated principal and tenant using your normal security model.
Public inbound correlation IDs
By default a syntactically safe public X-Correlation-ID is accepted so clients can correlate support requests. Disable that if you want only server-generated IDs:
'http' => [ 'accept_public_correlation_id' => false, ],
Incoming request IDs are never trusted; the package always creates a new local request ID.
Authentication / tenancy enrichment
The capture middleware is prepended globally, intentionally before authentication. If actor or tenant values only become available after auth/tenancy middleware, create an enricher:
use Abdulsalam\LaravelContextFlow\Contracts\ContextEnricher; final class AuthContextEnricher implements ContextEnricher { public function enrich(): array { return [ 'actor_id' => auth()->id(), ]; } }
Register it:
'enrichers' => [ App\Context\AuthContextEnricher::class, ],
Then place the middleware after authentication on the routes/groups that need it:
Route::middleware(['auth:sanctum', 'context-flow.enrich'])->group(function () { // ... });
Queue propagation
Laravel already serializes Context into queued jobs. This package hooks into Laravel's native dehydration/hydration lifecycle instead of adding properties to jobs.
When dispatching:
correlation_id = C1
execution_id = E1
The job starts as:
correlation_id = C1
execution_id = E2
causation_id = E1
A retry receives another execution ID while retaining the correlation lineage.
Queue compatibility policy
Laravel and third-party packages may store their own values in Context. Removing every unknown key would silently break them. Therefore the default is:
'queue' => [ 'unregistered_keys' => 'preserve', ],
Registered Context Flow keys still obey their configured targets and sensitive-looking keys are removed.
For a strict application in which you fully own every Context key:
'unregistered_keys' => 'drop',
Local-only values
Register a key with no propagation targets:
'debug_payload' => [ 'targets' => [], 'accept_from' => [], ],
Do not confuse Laravel hidden context with non-propagating context. Hidden context means it is hidden from logging; Laravel may still serialize it for queues. Context Flow applies propagation policy independently.
Scoped suppression
Disable propagation for a specific operation:
ContextFlow::withoutPropagation(function () { Http::post('https://example.com'); SensitiveJob::dispatch(); });
Or restrict application keys for a scope:
ContextFlow::only(['locale'], function () { SomeJob::dispatch(); });
The package uses Laravel Context's scoped hidden data for these flags, so it does not store request-specific mutable state in package singletons.
Scheduler and Artisan
Artisan commands receive a root context when none exists.
Scheduled tasks receive a fresh root context per execution, which prevents one iteration of schedule:work from leaking registered Context Flow values into the next task.
Logging and exceptions
Laravel already injects Laravel Context into logs. This package does not add another Monolog processor.
Log::info('withdrawal created');
will naturally include the Context Flow IDs through Laravel's Context logging integration.
Exception trackers can implement:
Abdulsalam\LaravelContextFlow\Contracts\ExceptionContextReporter
The package deliberately does not force Sentry/Bugsnag/Rollbar dependencies.
OpenTelemetry
Context Flow owns application correlation metadata, not tracing.
It never overwrites:
traceparent
tracestate
A TelemetryBridge contract and a no-op implementation are included so an application-specific bridge can be bound without coupling the package to one OpenTelemetry SDK version.
Baggage format
Application metadata is transported using a conservative W3C-baggage-compatible key/value representation:
baggage: locale=en-US,tenant_id=71
The built-in codec intentionally supports the common key/value subset. It does not attempt to interpret vendor-specific baggage properties.
Limits and overflow
Defaults:
'limits' => [ 'max_keys' => 32, 'key_bytes' => 128, 'value_bytes' => 1024, 'http_total_bytes' => 4096, 'overflow' => 'drop_low_priority', ],
In non-production environments the published config defaults to throw, which catches oversized contexts early. Production defaults to dropping lower-priority application keys before transport metadata.
Sensitive key protection
Common credential names are denied even if accidentally configured:
password
secret
token
api_key
authorization
cookie
session
cvv
private_key
This is a secondary safety net, not a replacement for explicit key registration.
Diagnostics
Inspect the current state:
php artisan context-flow:inspect
Validate configuration:
php artisan context-flow:doctor
The doctor checks signing configuration, risky public keys, sensitive-looking propagated keys, host configuration and header size limits.
Enable debug diagnostics:
CONTEXT_FLOW_DEBUG=true
Debug logging records boundary transitions, rejection reasons and propagated header names, but not baggage values.
Testing helpers
Use the provided trait in package/application tests:
use Abdulsalam\LaravelContextFlow\Testing\InteractsWithContextFlow; class WithdrawalTest extends TestCase { use InteractsWithContextFlow; public function test_context(): void { // ... $this->assertCorrelationIdExists(); $this->assertContextValue('tenant_id', 12); } }
The repository includes regression tests for UUID generation, baggage parsing, signing, trust resolution, snapshot filtering, execution rotation, queue filtering and HTTP injection.
Octane / long-running workers
No per-request values are stored in package singletons. Execution state lives in Laravel Context. At each HTTP/scheduled root boundary, package-managed and registered keys are reset before new IDs are created. Queue jobs rely on Laravel's hydration lifecycle, which flushes and hydrates Context per job.
If another package stores unscoped mutable state outside Laravel Context, that package must still handle its own worker reset lifecycle.
Extension points
Replace bindings in your application service provider when needed:
$this->app->bind( \Abdulsalam\LaravelContextFlow\Contracts\PropagationPolicy::class, App\Context\StrictPropagationPolicy::class, ); $this->app->bind( \Abdulsalam\LaravelContextFlow\Contracts\TrustResolver::class, App\Context\ServiceDiscoveryTrustResolver::class, );
Other extension contracts include IdGenerator, ContextEnricher, Carrier, TelemetryBridge and ExceptionContextReporter.
HTTP value semantics
Context values propagated through HTTP baggage are transport metadata and are reconstructed as strings on the receiving service. Keep cross-service context scalar, small, and type-agnostic; if your domain requires an integer or enum, validate and cast it at the application boundary. Queue propagation uses Laravel Context natively and therefore preserves serializable PHP value types.
Security model
Context Flow guarantees policy enforcement around its own registered keys and transports. It does not guarantee authenticity of unsigned headers, does not replace authentication/authorization, and cannot stop application code from manually sending sensitive data in unrelated HTTP headers.
See SECURITY.md for the threat model.
Development
composer install
composer test
php artisan context-flow:doctor
The GitHub Actions matrix tests Laravel 12/13 across compatible PHP versions.
License
MIT.