Creating a personalized, private RSS feed for users in Laravel

In building WP Lookout I had the need to create user-specific, private RSS feeds as a way for users to get updates and information specific to their account. Here’s how I did it.

I started with the laravel-feed package from Spatie, which allows you to easily generate RSS feeds, either from a model’s records or from some other method that you define. I found that someone has proposed a feature and submitted a PR to add support for signed routes, which is Laravel’s way of adding a hash key to a given URL so that it can’t be accessed if it’s been modified, perfect for something like a user-specific RSS feed. But the Spatie folks decided not to include the feature, so I had to figure out how to do it on my own.

First, I added the package to my project and published the related config file:

$ composer require spatie/laravel-feed
$ php artisan vendor:publish \
    --provider="Spatie\Feed\FeedServiceProvider" \
    --tag="config"

Then, I added a user-specific feed identifier to the User model:

$ php artisan make:migration add_feed_id_to_users_table --table=users

The migration looked like this in part:

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        //
        $table->string('feed_id');
    });

    DB::statement('UPDATE users SET feed_id = ...');
}

The UPDATE statement there is used to set the initial value of the feed identifier for existing users. An alternative would have been to make the field nullable and then set it elsewhere. For new users, I set the feed identifier as users are created, via the boot() method in the User model:

public static function boot()
{
    parent::boot();
    self::creating(
        function ($user) {
            $user->feed_id = ...;
        }
    );
}

The “…” in these cases represents the logic used to generate the private, per-user feed identifier that will be used in your feed URLs. Maybe it’s a hash of a unique piece of information about the user, or maybe something more complex.

Next, I updated the config/feed.php file with a few values specific to my application:

           'items' => 'App\TrackingEvent@getFeedItems',
           ...
           'url' => '/tracking-activity-feed'

This tells the laravel-feed package that I want the serving of my RSS feed route to be done in the TrackingEvent model using the getFeedItems() method. It also defines the base URI of my RSS feeds to be /tracking-activity-feed.

In my TrackingEvent model definition, I could now add that method and the associated toFeedItem() method that defined how the model would convert to an RSS item:

   // Define how to construct the RSS feed items based on the model properties
   public function toFeedItem(): FeedItem
    {
        // Usage info: https://github.com/spatie/laravel-feed
        return FeedItem::create()
            ->id('/tracking-events/' . $this->id)
            ->title($this->event_unique_title)
            ->summary($this->event_unique_title)
            ->updated($this->updated_at)
            ->link($this->trackingObject->main_url)
            ->author(config('app.name'));
    }

    // Fetch data to be included in the RSS feed
    public static function getFeedItems()
    {

        // Get the user from the URL
        $feed_id = request()->feed_id;
        $user = User::where('feed_id', $feed_id)->first();

        // Get the 20 most recent tracking events for this user and return them
        return $user->trackingEvents()
           ->latest()
           ->take(20)
           ->get();
    }

Now in my routes/web.php file, I can define the URL handler for the RSS feeds:

Route::get('/tracking-activity-feed/{feed_id}', ['uses' => '\Spatie\Feed\Http\FeedController'])
    ->name('feeds.main')
    ->middleware('signed');

This says to take any requests a URI like /tracking-activity-feed/1234 and (a) make sure it’s signed with a valid hash, and (b) pass it off to the Spatie FeedController class for further processing. If someone tries to access a feed URL without a valid signature, they’ll get a “403 Invalid signature” result.

Finally, I can generate some RSS feed URLs to display to my users so they can start accessing their feeds. Here’s the logic to generate a RSS feed URL for a specific logged in user:

$rss_feed_url = URL::signedRoute('feeds.main', ['feed_id' => auth()->user()->feed_id]);

The result that they see looks like this: https://app.example.com/tracking-activity-feed/1234?signature=5678

And when they access that URL in a browser or feed reader, they get their personalized results in an RSS format.

It’s worth saying that while the signed URL approach offers a bit of “security through obscurity,” it is not a good way to secure truly sensitive information, or to prevent the public from accessing resources within your application. A user could accidentally or intentionally share their URL with a third party, so some additional monitoring for unauthorized use may be in order. Related, exercises in caching and similar performance considerations are left to the reader.

For feeds of information that is not sensitive and that you want to make available in a convenient, standard format, I think this is a neat approach.

Published by

Chris Hardie

I'm a deep generalist, currently focused on software engineering + writing + the open web.

Leave a Reply

Your email address will not be published.