laravel-flowguard maintained by mykholy
Laravel Flowguard
Durable business-operation primitives for Laravel 12 and 13.
Stable contract: v1.0.0. Supports Laravel 12/13 and PHP 8.3/8.4/8.5, with SQLite, MySQL 8 and PostgreSQL 16 validation. The supported PHP API and persisted schema follow SemVer. This release has not received an independent human security audit and does not certify a host application's payment integration. Review the release scope and evidence before production use.
Flowguard solves a different problem from Telescope, Horizon and Pulse: who is allowed to execute a business operation, whether the outcome is known, and whether replay could be unsafe. It does not replace tracing/monitoring.
Included primitives
- Scoped idempotency claims with immutable payload fingerprints and owner-only transitions, including an explicit unknown-outcome state.
- Read-only reconciliation inspection, append-only evidence and optional authorization-gated manual outcome classification.
- Durable inbox receipts and atomic outbox recording within business transactions.
- Outbox leasing, bounded retry, fenced acknowledgement and a transport-neutral publisher contract.
- Read-only diagnostics for storage readiness and stale operations.
- MySQL/PostgreSQL independent-process contention and crash-point tests.
The host application still owns gateway signature verification, tenant security, publisher timeouts, remote idempotency and any real two-person approval workflow. Flowguard does not guarantee exactly-once effects across external systems.
Requirements and local development
PHP 8.3+, Laravel 12/13, a transactional database and Composer 2. CI tests Laravel 12/13 on PHP 8.3/8.4/8.5, plus MySQL 8 and PostgreSQL 16.
The package is published on Packagist. Install the supported 1.x series:
composer require mykholy/laravel-flowguard:^1.0
php artisan migrate
php artisan flowguard:doctor
Review the v1.0.0 release notes and upgrade procedure. Pin a released version in your lockfile and validate your own provider adapter before production deployment.
composer install
composer test
composer analyse
composer format-check
php tests/smoke.php
The service provider is registered using Laravel package discovery. Its migration is
loaded automatically and is applied by your normal php artisan migrate command.
To customize the connection/table, publish the config:
php artisan vendor:publish --tag=flowguard-config
php artisan migrate
php artisan flowguard:doctor
php artisan flowguard:outbox --max-attempts=10 --limit=50
php artisan flowguard:stale --minutes=15 --limit=50
flowguard:stale is read-only and shows old processing claims for investigation.
It never authorizes a replay or infers the external provider outcome.
Configure a durable transactional database with FLOWGUARD_DB_CONNECTION; defaults
to your Laravel application's default connection. Do not change connection/table
config after creating data without an intentional migration.
Example: payment capture
use Mykholy\Flowguard\Flowguard;
$guard = app(Flowguard::class);
$operation = $guard->begin(
scope: 'payments.capture',
idempotencyKey: $merchantId . ':' . $orderId,
payload: ['order_id' => $orderId, 'amount_minor' => $amountMinor, 'currency' => 'EGP'],
);
if (! $operation->acquired()) {
// NEVER call the gateway from this branch.
// Inspect $operation->operation->status and return an appropriate
// API response (completed / already processing / manual reconciliation).
return;
}
$claim = $operation->claim();
try {
// The gateway must support its own idempotency key where available.
$payment = $gateway->capture($orderId, $amountMinor, idempotencyKey: $merchantId . ':' . $orderId);
} catch (\Throwable $e) {
// An exception after dispatch may mean the provider already charged.
// Use provider-specific semantics for errors known to occur before dispatch.
$guard->requireReconciliation($claim, 'capture_outcome_unknown');
throw $e;
}
// Do not catch errors from complete() as if the provider capture failed.
// Persist local business writes atomically where required before this call.
// If finalization fails after capture, investigate without repeating the charge.
$guard->complete($claim, ['provider_reference' => $payment->reference]);
The caller must ensure errors before dispatch and known vs unknown outcomes are handled
according to provider semantics. A provider can charge successfully even when local
completion fails. Do not treat failed complete() as grounds for retrying the charge.
See RFC-001 and threat model.
Read-only reconciliation inspection
A provider-specific Reconciler may read the external state of an operation
explicitly marked requires_reconciliation. ReconciliationInspector returns
bounded, typed ReconciliationEvidence and cannot finalize or replay anything.
The caller must authenticate and authorize the inspection and use a read-only
provider lookup. This is intentionally not an automated recovery system.
See RFC-002 for the API and limits.
Opt-in durable reconciliation evidence journal
A provider observation is not permission to retry or complete a payment. Flowguard
can now persist a reduced, authorized audit observation on the same database
connection, while leaving the operation in requires_reconciliation.
- Requires an application-supplied
EvidenceAuthorizationpolicy. No default allow-all binding exists. Your policy must authenticate the operator and enforce tenant ownership; an actor string alone is not authentication. - Rechecks the database row under a transaction lock before inserting evidence.
- Stores typed observation metadata but not raw provider references, request payloads, headers or gateway responses.
- Does not implement replay, capture, refund, or finalization.
See the evidence journal design. Host policies and provider adapters need application-specific security and integration review.
Opt-in manual outcome classification
The optional DatabaseManualResolver can classify a previously unknown
external outcome as reconciled_success or reconciled_failure. These statuses
are bookkeeping only, and are NOT equivalent to completed application-side
writes. No payment, refund, capture, replay or business record mutation occurs.
It rejects stale, conflicting, inconclusive or superseded evidence; requires
two independently authenticated people and a bound approval verified by an
application-supplied ResolutionAuthorization policy; and atomically writes
a decision record with the status change. The package DOES NOT implement login
or collecting real two-person approvals. An actor-reference string alone is
not proof of identity or approval. Do not enable this in production without
an independently reviewed host-app policy.
Transactional outbox recording
DatabaseOutboxRecorder records a small, durable signal in the same database
transaction as a business change. It refuses to record outside an active
transaction. The caller must use the same database connection for both
writes and scope event identities by tenant. Duplicates fail through a database
unique constraint.
Recording does not publish or enqueue anything. A separately configured
OutboxDispatcher or flowguard:dispatch-once command can deliver a pending
message using an application-bound OutboxPublisher. There is no bundled
provider transport, automatic schedule or background daemon.
See outbox recording design.
Outbox reservation lifecycle
DatabaseOutboxLeaseStore allows a worker to reserve one pending message,
acknowledge it after successful publishing, or release it with a bounded retry
delay and machine-readable error code. Each lease carries an opaque token;
expired or replaced leases cannot acknowledge delivery. Attempts are capped
(default: 10), leaving exhausted records for operator inspection.
Consumers must support duplicate delivery. A publisher may succeed while its worker crashes before acknowledging the row. This is an at-least-once foundation, not exactly-once delivery. The host must supply a publisher and explicitly schedule its worker. See the lease contract and publishing guide.
Correctness and versioning policy
- Never repeat external effects when an existing operation is processing or unknown.
- No TTL-based automatic lock reclaim. Recovery requires reconciliation.
- Only application-verified, authorized, validated requests reach the engine.
- Do not store raw PII, gateway secrets, or complete response bodies in result metadata.
- The documented 1.x public API follows SemVer; no availability or delivery SLA is offered.
- Independent-process concurrency, crash injection and SQL restore tests run in CI. They do not replace a real-provider pilot or independent security review.
Contribution
See the roadmap, operations runbook and release criteria. Contributions should include a failing regression test, public failure scenario and rationale for any API change. Small, reviewable PRs preferred.
MIT © 2026 Ahmed El-Kholy.
API stability and upgrades
The supported public API contract is versioned from v1.0.0 and checked by signature and behavior tests. Operational retention and schema rollback policies are described in retention and the operations runbook.
Release artifacts
Clean-install CI installs the actual Composer distribution on every supported Laravel/database combination. See the distribution and publication procedure for archive verification, commit-linked artifacts and stable promotion steps.
Runnable checkout demo
See the Laravel checkout demo for an isolated host application, signed webhooks, independent HTTP provider, crash/timeout tests, and native full-host restore drills.