semitexa / ledger
Semitexa Ledger — SQLite append-only event ledger with NATS JetStream multi-node propagation
Requires
- php: ^8.4
- ext-sqlite3: *
- basis-company/nats: ^1.1
- semitexa/core: >=2026.09.13.1330 || dev-master
- semitexa/orm: >=2026.09.29.0833 || dev-master
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- 2026.09.29.0833
- 2026.09.28.1812
- 2026.09.27.0404
- 2026.09.23.1717
- 2026.09.13.1330
- 2026.09.13.0749
- 2026.09.08.2003
- 2026.08.04.2028
- 2026.07.11.0928
- 2026.07.06.1438
- 2026.06.21.0352
- 2026.05.08.1640
- 2026.04.15.1848
- 2026.04.15.0921
- 2026.04.14.1121
- 2026.04.12.1209
- 2026.04.09.1145
- v0.0.2
- v0.0.1
- dev-develop
This package is auto-updated.
Last update: 2026-09-29 15:13:11 UTC
README
semitexa/ledger adds an append-only SQLite event ledger with NATS-based cross-node propagation.
The package is opt-in at runtime. Keeping it installed in semitexa/ultimate no longer forces every app to provide ledger infrastructure immediately.
Enable It
Set these environment variables when you want the ledger to boot:
LEDGER_ENABLED=1 LEDGER_NODE_ID=store-a LEDGER_HMAC_KEY=change-me NATS_PRIMARY_URL=nats://nats:4222
Optional:
LEDGER_DB_PATH=/var/lib/semitexa/ledger/store-a.sqlite LEDGER_DB_CONNECTION=default NATS_SECONDARY_URL=nats://secondary:4222 EVENTS_DUAL_PRIMARY=nats EVENTS_DUAL_SECONDARY=<other-transport>
Propagate Events
Mark an event with #[Propagated] to persist it in the local ledger and publish it to other nodes.
use Semitexa\Core\Attribute\AsEvent; use Semitexa\Ledger\Attribute\Propagated; #[AsEvent] #[Propagated(domain: 'inventory')] final class StockAdjusted { private string $productId; private int $delta; public function getProductId(): string { return $this->productId; } public function getDelta(): int { return $this->delta; } }
Getter/setter DTOs are supported. Ledger payload serialization uses the same getter convention as the core PayloadSerializer. An event that keeps its data in public properties would serialise to an empty payload, so the writer refuses it.
Enforce Aggregate Ownership
Use #[OwnedAggregate] on propagated events and #[AsAggregateCommand] on commands that must execute on the owner node.
use Semitexa\Ledger\Attribute\AsAggregateCommand; use Semitexa\Ledger\Attribute\OwnedAggregate; use Semitexa\Ledger\Attribute\Propagated; #[Propagated(domain: 'inventory')] #[OwnedAggregate(type: 'product', idField: 'product_id', creates: true)] final class ProductCreated {} #[AsAggregateCommand(aggregateType: 'product', aggregateIdField: 'product_id')] final readonly class UpdateProductPrice { public function __construct( public string $product_id, public float $new_price, ) {} }
Replay Remote Events
Register an idempotent replay handler for events that must update the local main database.
use Semitexa\Ledger\Attribute\AsReplayHandler; use Semitexa\Ledger\Domain\Contract\ReplayHandlerInterface; use Semitexa\Ledger\Domain\Model\LedgerEvent; #[AsReplayHandler(domain: 'inventory', eventType: 'stock_adjusted')] final class StockAdjustedReplayHandler implements ReplayHandlerInterface { public function apply(LedgerEvent $event): void { // Update local projections idempotently using $event->eventId. } }
Check Replication
With the server running on every node:
bin/semitexa ledger:probe # on node A: append a probe event bin/semitexa ledger:status # on node B: events per origin, pending, unapplied, quarantined bin/semitexa ledger:status --probe=<probe_id> --json bin/semitexa system:doctor # ledger.multi-node: node-local defaults that break a cluster
tests/Harness/two-node/run.sh boots two full servers (own database, Redis and ledger each) against one JetStream server and checks both directions plus a burst.
The publisher, replayer and command listener run in worker 0 of each server. Set LEDGER_STREAM and LEDGER_SUBJECT_PREFIX per project when several projects share one NATS server.
CommandBus is not registered in the container yet: owner-routed commands wait for the ownership design.
Current Example In This Repo
packages/semitexa-demo/src/Application/Payload/Event/DemoItemCreated.php and DemoNotificationEvent.php are marked with #[Propagated(domain: 'demo')] as the first live integration inside this workspace.