Search by

siberfx / samsa

siberfx

Kafka's model of topics, partitions, offsets and consumer groups inside Laravel. Pure PHP, no broker required.

v1.4.0 2026-10-18 22:45 UTC

This package is auto-updated.

Last update: 2026-10-18 22:47:31 UTC


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

Tests Latest version PHP version License

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 the siberfx/message-bus transport (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-rdkafka C 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-pcntl is recommended so workers shut down gracefully on SIGTERM (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.

See When not to use Samsa.

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