siberfx / samsa
Kafka's model of topics, partitions, offsets and consumer groups inside Laravel. Pure PHP, no broker required.
Requires
- php: ^8.3
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/events: ^13.0
- illuminate/queue: ^13.0
- illuminate/support: ^13.0
- symfony/uid: ^7.2|^8.0
Requires (Dev)
- basis-company/nats: ^1.2
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.0
- predis/predis: ^3.7
- siberfx/message-bus: ^0.1
- symfony/process: ^7.2|^8.0
Suggests
- ext-pcntl: Required for graceful shutdown of workers on SIGTERM/SIGINT.
- ext-redis: For the Redis Streams driver (or use predis/predis).
- basis-company/nats: Required for the NATS JetStream driver.
- siberfx/message-bus: Use Samsa as a message-bus transport.
Provides
None
Conflicts
None
Replaces
None
README
Kafka's model of topics, partitions, offsets and consumer groups — inside your Laravel app.
Pure PHP. No broker required. No C extensions.
Getting started · Concepts · Documentation · FAQ
Shipped: the Database and memory drivers, producers, consumer groups, workers, retries, dead letters, the operations commands (1.0), the Laravel queue driver, event bridge,
Samsa::fake()and thesiberfx/message-bustransport (1.1), the NATS JetStream driver (1.2) and the Redis Streams driver (1.3). Everything is tested on SQLite, MySQL/MariaDB, PostgreSQL, NATS and Redis, including multi-process tests.
Gregor Samsa woke up one morning transformed. This is Kafka, transformed into a Laravel package.
Why Samsa?
You want an event log — durable, ordered, replayable, shared by several independent consumers — but:
- Kafka is a lot of infrastructure to run for one Laravel app,
- the Laravel Kafka packages need the
ext-rdkafkaC extension, and - Laravel's queues delete a job once it runs, so there's nothing to replay and only one consumer per job.
Samsa gives you the Kafka model on storage you already have:
// Producer: same key → same partition → strict order Samsa::produce('orders', ['id' => 42, 'total' => 99.90], key: 'order-42');
// Consumer: every group gets every record, at its own pace final class ChargeOrder implements Handler { public function handle(Record $record): void { Payment::charge($record->payload['id'], $record->payload['total']); } }
php artisan samsa:consume billing orders # the billing group php artisan samsa:consume emails orders # an independent group reading the same log
Features
| Feature | Status |
|---|---|
| Topics with a fixed number of partitions, ordering per key | ✅ 1.0 |
| Database driver (MySQL/MariaDB, PostgreSQL, SQLite) — produce inside your own DB transaction | ✅ 1.0 |
In-memory driver and the StorageDriverContract test suite for custom drivers |
✅ 1.0 |
| Consumer groups with per-partition offsets, replay and seek | ✅ 1.0 |
| Multiple workers with automatic partition rebalancing and crash takeover | ✅ 1.0 |
Retry with backoff, dead letter queue, samsa:failed:retry |
✅ 1.0 |
Lag reporting (samsa:status), retention (samsa:prune), producer dedup, OnlyOnce |
✅ 1.0 |
Samsa::fake() and test assertions |
✅ 1.1 |
Laravel queue driver — QUEUE_CONNECTION=samsa, no job changes |
✅ 1.1 |
Event bridge — #[PublishTo('orders', key: 'order_id')] on any event |
✅ 1.1 |
siberfx/message-bus transport ('driver' => 'samsa') |
✅ 1.1 |
| NATS JetStream driver — off-database, replicated, high throughput | ✅ 1.2 |
| Redis Streams driver — for apps already running durable Redis | ✅ 1.3 |
Requirements
- PHP 8.3+
- Laravel 13
- MySQL 8+ / MariaDB 10.6+, PostgreSQL 13+ or SQLite 3.35+, a NATS 2.10+ server with JetStream (
composer require basis-company/nats), or Redis 6.2+ ext-pcntlis recommended so workers shut down gracefully onSIGTERM(not available on Windows)
Installation
composer require siberfx/samsa php artisan vendor:publish --tag=samsa-config php artisan vendor:publish --tag=samsa-migrations php artisan migrate php artisan samsa:topic:create orders --partitions=8
Continue with the Getting started guide. It takes about 10 minutes from install to your first consumed record.
At a glance
use Siberfx\Samsa\Facades\Samsa; // Produce Samsa::produce('orders', $payload, key: $order->id); Samsa::topic('orders') ->key($order->id) ->withHeaders(['tenant' => $tenant->id]) ->dedupKey("order-placed-{$order->id}") ->produce($payload); // Produce atomically with your business data (Database driver) DB::transaction(function () use ($order) { $order->save(); Samsa::produce('orders', $order->toArray(), key: $order->id); }); // Subscribe a handler (or use config/samsa.php) Samsa::subscribe('billing', 'orders', ChargeOrder::class); // Forward domain events automatically #[PublishTo('orders', key: 'orderId')] final class OrderPlaced { public function __construct(public string $orderId, public float $total) {} } // Retry a flaky handler 5 times before it is dead-lettered #[Retry(maxAttempts: 5, backoffMs: [200, 1000, 5000])] final class ChargeOrder implements Handler { public function handle(Record $record): void { /* ... */ } }
php artisan samsa:consume billing orders --batch=100
php artisan samsa:status --group=billing
php artisan samsa:seek billing orders --to=time:2026-10-01T00:00:00Z # replay
php artisan samsa:failed
php artisan samsa:failed:retry --all
What Samsa is not
Samsa is honest about its scope:
- It does not replicate Kafka's distributed, multi-broker, replicated cluster. If you need that, run Kafka.
- Delivery is at-least-once. Handlers must be idempotent — see Idempotency.
- There are no transactions across topics and no exactly-once semantics.
- It is not a drop-in Kafka client. It borrows Kafka's model, not its wire protocol.
Documentation
| Start here | Getting started · Concepts · Configuration |
| Guides | Producing · Consuming · Queue driver · Event bridge · message-bus transport · Idempotency |
| Drivers | Database · NATS JetStream · Redis Streams |
| Running it | Operations & commands · Testing · FAQ |
License
MIT © siberfx