Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Stream Laravel Package

get-stream/stream

Official PHP SDK for Stream’s activity feed and chat APIs. Build scalable timelines, follow relationships, notifications, and real-time messaging in Laravel or any PHP app with a clean, well-documented client, feed groups, enrichment, and moderation support.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the right package name. The GitHub repo is GetStream/stream-php, but the Composer package is getstream/stream-php (namespace GetStream\Stream). composer require get-stream/stream will fail — there is no such package.

composer require getstream/stream-php

Pin the major version explicitly in composer.json ("getstream/stream-php": "5.*" or whatever resolves today). This library ships breaking changes between majors, and because it is in maintenance mode you will not get deprecation warnings ahead of time — only a changelog entry.

What you need from the dashboard: API key, API secret, app ID, and the app's region (e.g. us-east, eu-west, dublin). All four matter; a region mismatch yields 403s, not a helpful error.

First smoke test. Get a client, write to a feed group that already exists in the dashboard, read it back:

use GetStream\Stream\Client;

$client = new Client($apiKey, $apiSecret, $appId, $region, 10);

$feed = $client->feed('user', '1');   // slug, userId-as-string

$feed->addActivity([
    'actor'      => 'user:1',
    'verb'       => 'post',
    'object'     => 'post:42',
    'foreign_id' => 'post:42',
    'time'       => (new DateTime())->format(DateTime::ATOM),
]);

$activities = $feed->getActivities(20, 0);

Where to look first:

  1. src/Client.php — construction, feed(), batch(), users(), token creation, setOptions().
  2. src/Feed.php — everything you will actually call day to day: addActivity, getActivities, followFeed, addReaction, markRead.
  3. src/Batch.php — bulk write semantics.
  4. tests/ — the integration tests are the de-facto API documentation, and they are more current than the README.

Canonical first use case: a per-user timeline. Activities are added once to the actor's own feed; Stream fans out to followers. You never write the timeline yourself — you write to user:1 and read from timeline:1 after timeline:1 follows user:1.

Implementation Patterns

Bind it as a singleton in Laravel

Do not instantiate Client per request in controllers. One binding, one config file:

// config/stream.php
return [
    'key'     => env('STREAM_KEY'),
    'secret'  => env('STREAM_SECRET'),
    'app_id'  => env('STREAM_APP_ID'),
    'region'  => env('STREAM_REGION', 'us-east'),
    'timeout' => env('STREAM_TIMEOUT', 10),
];

// AppServiceProvider::register()
$this->app->singleton(Client::class, fn () => new Client(
    config('stream.key'),
    config('stream.secret'),
    config('stream.app_id'),
    config('stream.region'),
    config('stream.timeout'),
));

Add a thin FeedService wrapper around it if you want to fake it in tests — the SDK has no built-in fake, and mocking a concrete class with a Guzzle client inside is unpleasant.

Never ship the secret to the frontend

The browser/JS SDK needs a user token, not your API secret. Issue one server-side:

Route::get('/stream-token', fn () => response()->json([
    'token' => app(Client::class)->createUserToken((string) auth()->id()),
]));

Guard this route with auth. The user ID in the token must match the feed IDs you read from the client.

Read path: enrichment, reactions, pagination

$activities = $feed->getActivities(
    limit: 20,
    offset: 0,
    idGte: null,
    idLte: null,
    ranking: null,
    enrich: true,
    reactions: ['like' => 5, 'comment' => 5]
);
  • enrich: true pulls actor, object, and any other referenced entities out of the app's user/object store so you get display names and avatars without a second query. It only works if you actually wrote those records via $client->users()->add(...) or the equivalent — otherwise you get bare references back and wonder why actor.name is missing.
  • reactions is a map of reaction type → count. If you omit it, activity['latest_reactions'] simply won't exist. Most "reactions are broken" reports are this.
  • Pagination is cursor-based via activity ID, not offset. Prefer passing idLte from your last item's id over offset — offset drifts when new activities arrive mid-scroll. idGte is for polling "what's new since".

Write path: idempotency by construction

Always set foreign_id and time together. Stream deduplicates on the pair, which makes your queued job safe to retry:

$feed->addActivity([
    'actor'      => "user:{$user->id}",
    'verb'       => 'post',
    'object'     => "post:{$post->id}",
    'foreign_id' => "post:{$post->id}",
    'time'       => $post->created_at->toIso8601String(),
]);

Re-running this with identical values is a no-op. Changing time creates a duplicate — don't pass now() on a retry-safe job.

Fan-out: three distinct patterns

  1. Follow-based. $timelineFeed->followFeed('user', '1'). Use when the graph is user-driven (follows, teams, projects).
  2. Targeted adds. Pass 'to' => ['notification:1', 'timeline:2'] on the activity. Use for @-mentions and notifications where there is no follow relationship. Note it bypasses the follow graph entirely.
  3. Bulk. $client->batch() for imports and backfills:
$batch = $client->batch();
foreach ($rows as $row) {
    $batch->addActivity('user', $row['id'], $activity);
}
$batch->get(); // or ->followMany(...)

Keep batches in the low hundreds per call — chunk your import. Batch is the difference between a 10-minute backfill and a 10-hour one.

Notifications

Notification feeds carry unread/unseen counters alongside results. Read, then acknowledge explicitly:

$feed = $client->feed('notification', (string) $user->id);
$activities = $feed->getActivities(20, 0);
$feed->markRead();   // or markSeen() for "seen" semantics

Do the markRead() in a separate request from a "user opened the panel" event, not on every poll — it is a write, and writes cost more than reads.

Reactions as a comment system

$feed->addReaction($activityId, 'comment', $userId, ['text' => $body]);
$feed->addChildReaction($commentReactionId, 'like', $userId);
$feed->getReactions($activityId, 'comment', limit: 20, idLte: $cursor);
$feed->updateReaction($reactionId, ['text' => $edited]);

Comment threading is one level (addChildReaction). If you need deep threads, model them as parent reactions in your own DB and use Stream only for the top level.

Testing

Wrap the client behind an interface (ActivityFeedGateway) with two implementations: the real one and an in-memory one for tests. Trying to Mockery::mock(Client::class) and assert on chained calls gets brittle fast, and the SDK constructs its own Guzzle handler. A small fake also lets you test your pagination and enrichment assumptions without network calls.

Gotchas and Tips

Maintenance mode is the headline. This is Feeds v2. New projects should use getstream/getstream-php (Feeds v3), which has a different client, different feed model, and no to targets. If you are starting today, take the v3 hit now — migrating a live app later means rewriting every read and write call site plus re-syncing activity data. If you are already on v2, the practical question is whether v2 gets security patches long enough for your roadmap; plan an exit rather than assuming indefinite support.

Feed groups must pre-exist. $client->feed('team', '1') where team was never created in the dashboard fails at request time with a not-found style error. There is no "create group via SDK". This is the single most common first-hour confusion.

Everything is a string. User IDs, activity IDs, feed slugs. $client->feed('user', 1) may work by coercion in some paths and produce silently wrong feed names in others, and user:1 vs user:1.0 from a float-cast is a real bug in PHP. Cast at the boundary.

Default timeout is ~3 seconds. Fine for a single addActivity, hopeless for a batch or an enriched read with reactions on a cold cache. Bump it to 10 and treat timeouts as retryable.

Region mismatch returns 403, not a redirect. Set region to exactly what the dashboard shows. EU apps hitting the US endpoint look like auth failures but are data-residency rejections.

Exceptions are worth catching specifically. The SDK throws StreamAPIException / StreamFeedException (extending a base StreamException). Distinguish:

  • 4xx other than 429 → your bug, don't retry, log the response body (it contains a detail and code that the generic message omits).
  • 429 → back off with jitter; the SDK does not retry for you.
  • 5xx / timeouts → retry, but only for writes you made idempotent with foreign_id + time.

Removing activities: prefer deletion by foreign_id (removeActivityByForeignId) over by activity ID. Your database knows the foreign ID; it usually doesn't know Stream's ID, and fetching it just to delete is a wasted round trip that can race.

getActivities() returns a raw results array, not a page object. Notification feeds return counters alongside results in the underlying response; helper methods may unwrap that for you. If you need unread counts reliably, capture the raw response or call markRead()/markSeen() defensively — don't infer counts from count($activities).

Enrichment silently degrades. If an actor was never stored in Stream's user store, enrichment returns the bare reference and your Blade template renders an empty name. There is no error. Backfill users before you backfill activities.

Log the request ID. Attach a PSR-3 logger via the client's logger option and include the exception message — Stream support will ask for the request ID from the error body, which the PHP exception often wraps inside a decoded JSON payload rather than the message string.

Small ecosystem. ~140 stars and effectively no downstream dependents means: no Stack Overflow corpus, few blog posts, and Copilot/LLM autocomplete will confidently hallucinate method names (especially around reactions and unread counts). When in doubt, read src/Feed.php — it is a few hundred lines and is the only authoritative reference. Verify signatures against the installed version with ReflectionClass before trusting any snippet, including the ones above.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo