pusher/pusher-chatkit-server
Official PHP server SDK for Pusher Chatkit, the hosted chat API. Create and manage users, rooms, and messages, set permissions, and add real-time chat to your Laravel or PHP backend without building the infrastructure yourself. Provides a clean client for Chatkit's REST API.
This is a thin, archived HTTP wrapper around Pusher's (now shut down) Chatkit API. Treat it as legacy integration code, not a package to adopt. If you've inherited it, the goal is to understand its day-to-day surface so you can keep it running or migrate off it deliberately.
Install and configure
composer require pusher/pusher-chatkit-server
// config/services.php
'chatkit' => [
'instance_locator' => env('CHATKIT_INSTANCE_LOCATOR'), // v1:us1:xxxx
'key' => env('CHATKIT_KEY'),
],
Bind it once
// app/Providers/ChatkitServiceProvider.php
public function register(): void
{
$this->app->singleton(Chatkit::class, fn () => new Chatkit([
'instance_locator' => config('services.chatkit.instance_locator'),
'key' => config('services.chatkit.key'),
]));
$this->app->bind(ChatkitClient::class, ChatkitSdkClient::class); // your interface
}
Never new Chatkit(...) in a controller — wrap it behind your own interface so you can swap implementations and fake it in tests.
Where to look first
README.md — every method has a copy-paste example; this is effectively the documentation.src/Chatkit.php — the complete API surface (users, rooms, messages, cursors, auth).tests/ — expected request/response shapes, including status codes.First use case: an auth endpoint for the Chatkit JS client
The most common day-to-day call is issuing a token for the browser SDK:
// routes/api.php
Route::post('/chatkit/auth', [ChatkitAuthController::class, 'issue'])
->middleware('auth:sanctum');
// controller
public function issue(Request $request, Chatkit $chatkit)
{
$response = $chatkit->authenticate(['userId' => (string) $request->user()->id]);
return response()->json($response['body'], $response['status']);
}
The SDK returns a status/body array; forward the body as-is so you don't break the JS client's expected token shape.
Keep your user IDs stable and non-PII. Chatkit user IDs are strings you control. Use a UUID (or (string) $user->id if your keys never change). If you use email-derived IDs, renames and GDPR deletions become a data migration.
Mirror lifecycle events into Chatkit with queued jobs. Registration, profile updates, and account deletion should not block on an HTTP round trip:
// app/Jobs/CreateChatkitUser.php
public function handle(Chatkit $chatkit): void
{
$chatkit->createUser([
'id' => $this->user->uuid,
'name' => $this->user->name,
'avatar_url' => $this->user->avatarUrl(),
'custom_data' => ['plan' => $this->user->plan],
]);
}
Dispatch from Registered/Updated model events. Make the job idempotent — a 4xx "already exists" on retry should be treated as success.
Store room IDs in your own schema. Chatkit generates room IDs; you can't derive them from your own records.
$response = $chatkit->createRoom([
'creator_id' => $project->owner->uuid,
'name' => $project->name,
'user_ids' => $project->members->pluck('uuid')->all(),
]);
$project->update(['chatkit_room_id' => $response['body']['id']]);
Then membership changes map 1:1 to addUsersToRoom / removeUsersFromRoom from the same listeners that manage your pivot table.
Use batch endpoints for backfills. createBatchUsers and createBatchRooms exist for seeding and imports. Keep batches in the low hundreds and run them from a job with backoff — they count against the same rate limit as everything else.
Server-initiated messages are a feature, not a hack. sendMessage from Laravel is how you deliver system events ("Alice added Bob", "Deploy finished") into rooms without faking a user client. Pair with deleteMessage for takedown/moderation flows, and getMessages for admin history views.
Cursors for unread counts. setReadCursor writes a read position on behalf of a user. If you're rendering unread badges server-side, read/write cursors through the SDK rather than duplicating state.
Wrap, don't sprinkle. A single ChatkitClient interface with methods like provisionUser(), openRoom(), addMember(), systemMessage() gives you one place to add logging, retries, and error mapping — and one class to fake in tests:
$this->instance(ChatkitClient::class, new FakeChatkitClient());
Authenticate tokens per request, never persist them. They're short-lived by design and the JS SDK refreshes by re-hitting your endpoint. Caching them server-side only creates staleness bugs.
The service is gone and the package is archived. Pusher sunset Chatkit; there is no live backend for this SDK. If this is a greenfield decision, stop — the correct move is to keep your chat domain in your own database and use Pusher Channels (or Sendbird/Stream/Ably) for transport. If you inherited it, the SDK is now a specification for the migration: your ChatkitClient interface is the seam to swap.
Dependency conflicts on modern Laravel. The package targets older PHP/Guzzle/PSR versions and will fight Laravel 10/11's dependency tree. Check before installing:
composer why-not pusher/pusher-chatkit-server
If you truly need it, fork it into your own VCS and bump the constraints rather than passing --ignore-platform-reqs on the whole project.
Raw transport exceptions leak. The SDK throws Guzzle/Package exceptions on non-2xx. Centralize handling and map to domain errors:
try {
return $chatkit->createUser($payload);
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() === 409) {
return ['status' => 200, 'body' => []]; // already exists, idempotent
}
throw new ChatkitUnavailable($e->getMessage(), previous: $e);
}
Never log the Authorization header or the instance key, and scrub request bodies from exception context.
Respect 429s. Wrap calls in a retry with exponential backoff and jitter; queue anything that isn't on the critical path. Bulk operations without backoff are the fastest way to get throttled.
custom_data is a small JSON blob, not a database. Keep it to display hints (plan, role label). No nested history, no growing arrays — every read of a user/room ships it, and size caps exist.
Deleting a user is irreversible and cascades. It removes them from rooms and their message attribution degrades. Prefer soft-deletion in your own DB and leave the Chatkit user intact unless you're satisfying an erasure request — and if so, do it from a job with an audit log.
Avatar URLs must be publicly reachable. Signed/expiring S3 URLs will render broken in the client. Generate a stable public URL or a CDN path.
Octane and config caching. The singleton is constructed once per worker. That's fine for static credentials, but if you rotate keys, app()->forgetInstance(Chatkit::class) on the rotation event — otherwise workers keep signing with the old key until restart. Similarly, make sure config:cache has run before deployment so the provider reads real values.
No webhooks in the SDK. There are no server-side event delivery helpers; the Chatkit model was client-push. If your design depends on server-side "message sent" events, you'll need to poll getMessages or move that logic to the client/socket layer.
Extension point is your own layer, not the package. The class is a hard-to-extend HTTP client. Fork only for compatibility fixes; put behavioral concerns (retries, idempotency, metrics, audit logging) in the decorator around your ChatkitClient interface, where they're testable and survive a backend swap.
How can I help you explore Laravel packages today?