mrezdev / laravel-talkto
Reliable Laravel service-to-service messaging with signed outbox/inbox envelopes, idempotency, retries, callbacks, observability, and security audit tooling.
Requires
- php: ^8.2
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/queue: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- illuminate/validation: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.10
- laravel/pint: ^1.29
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
README
Laravel Talkto helps Laravel applications communicate through reliable, signed, durable service-to-service messages. It is built for projects where multiple Laravel apps need to send commands, receive results, retry safely, and stay observable without turning every app into a copy of the others.
Talkto owns the communication boundary: envelopes, signatures, durable message records, retries, dead letters, callbacks, and operational visibility. Your host applications still own business rules, validation, payload meaning, permissions, model lookup, data mapping, dashboard policy, and what each result means.
It helps teams move toward a more modular, microservice-friendly Laravel architecture without forcing a full event-streaming platform at the start.
Why Laravel Talkto?
Direct HTTP calls between applications can become hard to reason about once retries, failures, duplicate delivery, security checks, and follow-up results enter the picture. Talkto gives Laravel apps a clearer communication boundary where messages can be signed, stored, retried, inspected, and completed with callbacks.
Each Laravel app can stay focused on its own responsibility. Talkto handles the shared transport concerns, while the host app decides what a command means and how its own data should change.
From two apps to a distributed Laravel ecosystem
Talkto can start with two Laravel apps talking to each other. The same model can also support several Laravel apps connected through clear commands, signed envelopes, retries, dead letters, callbacks, and observability.
This helps teams keep applications more independent while still allowing them to work together through explicit, durable service conversations.
What It Does
Laravel Talkto helps one Laravel app send a command to another Laravel app, persist the lifecycle on both sides, verify the message, run an approved handler, and record enough state for retries and operations.
It is useful when direct HTTP calls have become hard to reason about, but a full event streaming platform would be too much.
When To Use It
- You have Laravel services that exchange commands.
- You need HMAC signatures, timestamp checks, payload hashes, and nonce replay protection.
- You want durable outbox/inbox records for message lifecycle and operations.
- You need idempotency, retries, dead-letter handling, and trace/report commands.
- You want host-owned command handlers with package-owned transport behavior.
When Not To Use It
- You only need an in-process service class call.
- You need a general pub/sub broker, stream processor, or event sourcing system.
- You want the package to own host business workflows or data mapping.
- You cannot run queues or persist package message tables.
- You are not ready to configure per-peer secrets and command allowlists.
Installation
Install from Packagist:
composer require mrezdev/laravel-talkto
Publish the config and migrations when the host app is ready to review them:
php artisan vendor:publish --tag=laravel-talkto-config php artisan vendor:publish --tag=laravel-talkto-migrations php artisan migrate
Short publish tags are also available:
php artisan vendor:publish --tag=talkto-config php artisan vendor:publish --tag=talkto-migrations
Published Talkto migration copies use Laravel's standard migration publisher. Current Laravel 12/13 applications normally set database.migrations.update_date_on_publish=true, so newly published copies receive current, unique, sequential timestamps while the package source filenames stay stable. If Talkto migrations already exist in the host app, keep those reviewed files unless you intentionally republish and inspect the result.
Panel Blade views are optional and publishable with:
php artisan vendor:publish --tag=talkto-panel-views
Local path or VCS installation is only for package development or private testing. Normal users should install with composer require mrezdev/laravel-talkto.
See docs/installation.md for the full install flow.
5-Minute Quickstart
Set a stable local service name:
TALKTO_SERVICE=website-service TALKTO_MIGRATIONS_ENABLED=false TALKTO_ROUTES_ENABLED=false
Configure the source app with an outgoing target:
'outgoing' => [ 'inventory-service' => [ 'base_url' => env('TALKTO_INVENTORY_URL'), 'receive_endpoint' => '/api/talkto/receive', 'secret' => env('TALKTO_TO_INVENTORY_SECRET'), 'callback_endpoint' => '/api/talkto/callback', ], ],
Configure the receiving app with an incoming source and command allowlist:
'incoming' => [ 'website-service' => [ 'secret' => env('TALKTO_FROM_WEBSITE_SECRET'), 'allowed_commands' => [ 'catalog.reserve-stock' => [ 'driver' => 'handler', 'handler' => App\Talkto\Handlers\ReserveStockHandler::class, 'idempotency' => 'required', ], ], 'allow_all_commands' => false, ], ],
Run a queue worker in each app that processes Talkto jobs:
php artisan queue:work
Secure Defaults
New installs use v2 signatures by default:
TALKTO_SIGNATURE_VERSION=v2 TALKTO_ACCEPT_SIGNATURE_VERSIONS=v2 TALKTO_REQUIRE_V2_NONCE=true
v2 requests include a timestamp, payload hash, signature, and signed nonce. The nonce is consumed by a replay ledger that stores nonce hashes only, not raw nonce values. v1 remains available only as an explicit legacy/manual opt-in.
Talkto also validates protocol identifiers and Talkto headers before signing, sending, receiving, nonce storage, and callback application. Service names, commands, message ids, correlation ids, parent ids, timestamps, nonces, signature-version values, and payload hashes may use ordinary Unicode text, spaces, slashes, dots, colons, hyphens, and underscores, but they cannot contain ASCII control characters, DEL, Unicode line/paragraph separators, or invalid UTF-8. HTTP header names must use RFC token characters, so configured Talkto header names and custom target headers cannot contain spaces, colons, slashes, control characters, or Unicode.
Do not disable signatures, nonce replay protection, or command allowlists in production. See docs/security.md and docs/production-hardening.md.
Outgoing HTTP TLS certificate verification is also enabled by default. Use a CA bundle for private certificate authorities instead of disabling verification in production; talkto:security-audit and the panel surface risky SSL settings.
Sending Commands
Use TalktoOutgoingMessageFactory to create a durable outgoing message:
use Mrezdev\LaravelTalkto\Services\TalktoOutgoingMessageFactory; $message = app(TalktoOutgoingMessageFactory::class)->create( target: 'inventory-service', command: 'catalog.reserve-stock', payload: ['sku' => 'sku-123', 'quantity' => 2], options: [ 'business_key' => 'cart-123', 'idempotency_key' => 'website-service:catalog.reserve-stock:cart-123:v1', ], );
Delivery is handled after the message is stored, usually by a queue worker and retry policy. Host apps decide when to create their own business record and how to correlate it with the Talkto message.
Envelope metadata and target headers are validated before the outgoing row is written where practical. Unsafe control characters in the source service, resolved target service, command, message id, correlation id, parent message id, configured header name, or custom target header fail with a package exception and no message, event, attempt, nonce, job, or HTTP request is created. Historical rows or late target config changes fail locally and non-retryably before HTTP. This validation is not applied to business payload strings, so multiline user content, tabs, trailing spaces, empty strings, and localized text remain valid payload data.
Before an outgoing row is written, Talkto freezes the host-supplied payload into JSON-safe primitives. Each supported object instance is converted once per freeze operation, even when the same instance appears in multiple payload paths. The stored payload, payload hash, signed envelope, HTTP body, retries, DLQ rows, callbacks, and hash repair all reuse that frozen tree. Direct callback data snapshots supplied to TalktoResultCallbackData are also validated and frozen before toPayload() or toEnvelope() can reuse them. Already-primitive JSON payloads keep the same deterministic hashes; unsupported runtime values such as closures, resources, generators, internal/traversable hidden-state objects, native DateTimeInterface, pure enums, invalid UTF-8, non-finite floats, circular references, and excessive nesting fail before persistence. Carbon and other JsonSerializable date objects keep their JSON representation.
Receiving Commands
Incoming commands are accepted only from configured sources and only when the command is allowed. A minimal handler implements TalktoIncomingCommandHandler:
use Mrezdev\LaravelTalkto\Contracts\TalktoIncomingCommandHandler; use Mrezdev\LaravelTalkto\Models\TalktoMessage; use Mrezdev\LaravelTalkto\Services\TalktoIncomingCommandResult; final class ReserveStockHandler implements TalktoIncomingCommandHandler { public function handle(TalktoMessage $message): TalktoIncomingCommandResult { $payload = $message->payload ?? []; if (! isset($payload['sku'])) { return TalktoIncomingCommandResult::failedFinal('Missing sku.'); } return TalktoIncomingCommandResult::succeeded([ 'reserved' => true, 'sku' => $payload['sku'], ]); } }
The package verifies the envelope, stores the incoming row, applies idempotency rules, and dispatches or runs the configured handler. The handler owns the business behavior.
Result Callbacks
Callbacks let the destination app report handler results back to the source app. The generic signed callback runtime uses the same signed-envelope model, including v2 nonce replay protection.
Result callbacks are durable: the destination stores an outgoing talkto.result callback message in talkto_messages, queues SendTalktoMessage, and delivers the callback through the normal outgoing send pipeline. Callback delivery can use the existing retry, DLQ, report, panel, and reprocess behavior.
Incoming processing auto-queues callbacks by default after a handler returns a TalktoIncomingCommandResult, so normal handlers only need to return the result:
use Mrezdev\LaravelTalkto\Services\TalktoIncomingCommandResult; return TalktoIncomingCommandResult::succeeded(['reserved' => true]);
Manual ResultCallbackSenderContract::sendResult($message, $result) is still supported for advanced flows and returns queued delivery details such as sent=false and queued=true. Duplicate calls for the same original message/status reuse the deterministic durable callback message and suppress duplicate queue dispatch where the existing callback row/events show delivery is already queued or handled. The source app must configure the destination as an incoming source and allow the callback command, which defaults to talkto.result. Package callback routes depend on talkto.routes.enabled and talkto.callbacks.enabled; host-owned routes can call the receiver contract directly when package routes stay disabled.
Retry, DLQ, And Observability
Retry state is stored on message records and processed with:
php artisan talkto:retry-failed --dry-run
Final failures can be stored in the dead-letter table and reviewed or reprocessed deliberately:
php artisan talkto:dlq-reprocess --dry-run
Read-only operational commands:
php artisan talkto:report --hours=24 --direction=all --limit=20 php artisan talkto:trace <message-id> php artisan talkto:security-audit php artisan talkto:audit-security
Use talkto:security-audit as the main detailed security audit command. talkto:audit-security is also registered as a PASS/WARN/FAIL compatibility audit command.
Optional Panel
Laravel Talkto includes an optional Blade operations panel. It is disabled by default.
TALKTO_PANEL_ENABLED=false
If you enable it, keep every panel route behind host-owned auth/admin middleware and the configured gate. Keep payload and response visibility disabled unless operators are explicitly allowed to view that data. See docs/panel.md.
The message list includes both the detailed technical Status filter and a business-level Completion state filter for reviewing completed versus not-completed operations without selecting each lifecycle status manually.
Testing And Local Validation
For a package checkout:
composer install composer validate --strict composer audit vendor/bin/pint --test vendor/bin/phpstan analyse vendor/bin/pest
For a host integration, also test one outgoing command, one incoming command, one duplicate message_id, one callback if used, one retry dry run, one trace/report command, and the security audit.
Documentation Map
Start with docs/README.md. The full documentation index is a technical map with grouped links for setup, concepts, examples, operations, package development, and support.
Common next stops:
- Installation
- Configuration
- Security
- Production hardening
- Architecture
- Outgoing-only example
- Incoming-only example
- Bidirectional callback example
- Result callbacks
- Retry, DLQ, and recovery
- Troubleshooting
- Public API
- Upgrade guide
Security
Report suspected vulnerabilities through the repository security advisory workflow or the maintainer-approved public security contact for the published package. Do not include real secrets, production payloads, raw signatures, raw nonce values, cookies, authorization headers, or private service URLs in reports.
See SECURITY.md.
Versioning, Changelog, License, And Support
Laravel Talkto is licensed under the MIT license. Review CHANGELOG.md, UPGRADE.md, and SUPPORT.md before upgrading or opening support requests.


