InfraRead (pronounced infrared ) is an elegant self-hosted RSS feed reader and sync service. It is optimized for simplicity, reading and comfort. It is inspired by Unread and built with Laravel and VueJs.
- Distraction free. Attention to details and optimized for reading
- Access From any device and feeds stay synced
- Import your own OPML or add feeds manually
- 100% responsive
- Full Keyboard Navigation using Vim Shortcuts (Key bindings can be changed)
- Save For later, can choose between Instapaper and Pocket
- Offline persistence of settings and previously loaded posts
- Complete REST API for programmatic access and external client development
InfraRead provides a comprehensive REST API for all functionality. See API-REFERENCE.md for complete documentation including:
- Authentication with personal access tokens
- Posts management (read/unread, bulk operations, filtering)
- Sources and categories CRUD
- OPML import/export for RSS reader migration
- Rate limiting and error handling
- Complete examples and workflows
- Same Requirements as Laravel 8
- Ability to create Cron Jobs
- Queue worker for background jobs (database queue driver recommended). In production (Ubuntu), run a persistent systemd service; in macOS development, run
php artisan queue:workin a terminal.
- Download or clone the repository to your server
- run
composer install - run
php artisan key:generate - make a copy of
.env.exampleand save as.env - fill your admin details in
.env - fill your database details in
.env - run
php artisan migrateto migrate the database - run
php artisan db:seed, this will populate your admin details in the database - add this line to your Crontab:
* * * * * php /path/to/your/site/artisan schedule:run >> /dev/null 2>&1 - If you want to modify the js and css assets, also run:
npm install, and thennpm run dev
On macOS (development) without cron, you can run the scheduler loop instead:
php artisan schedule:work
InfraRead uses Laravel's queue for background jobs (e.g., on‑demand feed refresh, summaries). With the database queue driver you don't need Redis/Horizon—just a long‑running worker.
- Create a systemd service (adjust paths, user, and env):
File: /etc/systemd/system/infraread-queue-worker.service
[Unit]
Description=Infraread Laravel Queue Worker
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/infraread/current
Environment=APP_ENV=production
Environment=QUEUE_CONNECTION=database
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --timeout=120 --max-time=3600
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
- Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now infraread-queue-worker
sudo systemctl status infraread-queue-worker
Notes
- Ensure queue tables exist:
php artisan queue:table && php artisan migrate(run once). - Tune
--timeout,--tries, and--sleepbased on workload. - Tail logs:
journalctl -u infraread-queue-worker -f. - macOS/dev: you can simply run
php artisan queue:workin a terminal; systemd isn’t available on macOS.
For local development using Laravel Herd on macOS:
- Ensure
.envcontainsQUEUE_CONNECTION=databaseand migrate the queue tables once:
php artisan queue:table
php artisan migrate
- Run the queue worker in a terminal tab:
php artisan queue:work --sleep=3 --tries=3 --timeout=120
- Run the scheduler loop in another tab (instead of setting up cron locally):
php artisan schedule:work
Herd provides PHP and web serving on macOS; you don’t need systemd. Keep these two processes running while developing.
Minimal queue-related settings:
# Common
QUEUE_CONNECTION=database
# Optional: increase if your feed fetch/summarization can take longer
# These are passed as CLI flags to queue:work; shown here for reference
# WORKER_SLEEP=3
# WORKER_TRIES=3
# WORKER_TIMEOUT=120
Recommended worker flags
- Ubuntu (systemd ExecStart):
queue:work --sleep=3 --tries=3 --timeout=120 - macOS dev (terminal):
queue:work --sleep=3 --tries=3 --timeout=120
Tune --timeout based on the slowest expected job (e.g., long feeds); avoid setting it too high to keep stuck jobs from blocking the worker.
InfraRead processes RSS feeds through a combination of scheduled batch processing (cron) and on-demand jobs. The system includes comprehensive error handling, health monitoring, and exponential backoff for failed feeds.
Update all active feeds:
php artisan app:update_postsUpdate a specific feed:
php artisan app:update_posts {source_id}The command provides real-time progress reporting with:
- 🔄 Processing indicators for each feed
- ✓ Success confirmations with post counts
- ✗ Error notifications with details
- ⏳ Backoff notifications for temporarily disabled feeds
- 📊 Comprehensive summary with statistics
InfraRead tracks detailed metrics for each feed source:
Health Status:
active- Feed processing normallyfailing- Recent failures, but still being attemptedfailed- Multiple consecutive failures, in exponential backoff
Exponential Backoff: When feeds fail repeatedly, the system automatically implements exponential backoff:
- 1st failure: retry in 2 minutes
- 2nd failure: retry in 4 minutes
- 3rd failure: retry in 8 minutes
- Up to 24 hours maximum delay
Metrics Tracked:
last_fetched_at- When the feed was last successfully processedlast_fetch_duration_ms- Processing time in millisecondsconsecutive_failures- Number of failures since last successlast_error_at- When the most recent error occurredlast_error_message- Details of the most recent error
View detailed processing information:
# Check application logs
tail -f storage/logs/laravel.log
# Check for feed processing entries
grep "Feed processing" storage/logs/laravel.log
# Check for errors
grep "ERROR" storage/logs/laravel.log | grep -i feedLog Entries Include:
- Feed processing summaries with statistics
- Individual source success/failure details
- Error context for debugging (HTTP status, XML parsing issues, etc.)
- Performance metrics and processing duration
Common Issues and Solutions:
-
HTTP Errors (404, 500, etc.)
- Check feed URL is still valid
- Verify the website is accessible
- Look for redirects or URL changes
-
XML Parsing Errors
- Feed may have malformed XML
- Check for encoding issues
- Verify RSS/Atom format compliance
-
Timeout Issues
- Feed server may be slow
- Increase timeout in configuration if needed
- Monitor network connectivity
-
Plugin Errors
- Check plugin configuration
- Review plugin-specific error messages
- Verify plugin dependencies
Manual Recovery:
# Reset a failed source (via tinker)
php artisan tinker
>>> $source = App\Models\Source::find(123);
>>> $source->update(['status' => 'active', 'consecutive_failures' => 0]);
>>> $source->updatePosts(); // Try processing immediatelyDatabase Indexes: The system includes optimized database indexes for:
- Posts filtering by read status, source, and category
- Source health and metrics queries
- Date-based post filtering
Processing Statistics: Each command run provides detailed statistics:
- Total sources processed
- Success/failure counts
- Sources skipped due to backoff
- Total posts processed
- Processing duration
Monitoring in Production:
- Set up log monitoring for "Feed processing completed" entries
- Monitor consecutive failure counts
- Watch for sources stuck in "failed" status
- Track processing duration trends
InfraRead features a comprehensive plugin system for post-processing RSS content. Plugins can modify posts after they're fetched but before they're displayed, enabling content transformation, link fixing, automatic tagging, and more.
FixRelativeLinks - Converts relative links and images to absolute URLs
- Purpose: Fixes broken images and links in RSS feeds that use relative paths
- Options:
convert_images(bool): Convert relative image URLs (default: true)convert_links(bool): Convert relative link URLs (default: true)remove_srcset(bool): Remove srcset attributes (default: true)
MakeTextLegible - Improves text readability and formatting
- Purpose: Breaks up long paragraphs, removes ads, and cleans formatting
- Options:
min_letters_count(int): Minimum paragraph length to split (default: 700)max_paragraph_length(int): Maximum allowed paragraph length (default: 1000)remove_ads(bool): Remove advertisement content (default: true)clean_formatting(bool): Clean excessive formatting (default: true)
MarkPostAsRead - Auto-marks posts as read based on patterns
- Purpose: Automatically marks certain posts as read based on title/source patterns
- Options:
blacklist(array): Array of url/string patterns to match for auto-reading
ReplaceArticleLink - Replaces article URLs with embedded links
- Purpose: For curated feeds, replaces the feed URL with the first external link found
- Options:
exclusions(array): Strings to exclude from link replacementselector(string): CSS selector for finding links (default: "article a")
Plugins are configured in app/Plugins/Kernel.php with source-specific settings:
'plugins' => [
'kottke.org' => [
'FixRelativeLinks' => [
'convert_images' => true,
'convert_links' => true,
'remove_srcset' => true
],
'MakeTextLegible' => [
'max_paragraph_length' => 800
]
]
]List Available Plugins:
php artisan plugins:manage listValidate Plugin Configuration:
php artisan plugins:manage validateTest Plugin on Specific Source:
php artisan plugins:manage test --source=123View Sources with Plugin Configuration:
php artisan plugins:manage sources- Create Plugin Class in
app/Plugins/implementingPluginInterface:
<?php
namespace App\Plugins;
use App\Models\Post;
class MyCustomPlugin implements PluginInterface
{
private $post;
private $options;
public function __construct(Post $post, array $options = [])
{
$this->post = $post;
$this->options = $options;
}
public function handle(): bool
{
try {
// Your plugin logic here
$this->post->save();
return true;
} catch (\Exception $e) {
return false;
}
}
public function getMetadata(): array
{
return [
'name' => 'My Custom Plugin',
'description' => 'What this plugin does',
'version' => '1.0.0',
'author' => 'Your Name',
'modifies' => ['field1', 'field2'],
'options' => [
'option1' => 'Description of option1'
]
];
}
}- Add to Kernel Configuration in
app/Plugins/Kernel.php - Test with Management Commands
- Plugins log failures with context preservation
- Failed plugins don't prevent other plugins from running
- Comprehensive error tracking in
storage/logs/laravel.log - Plugin execution statistics and timing available
The application now exposes a comprehensive versioned JSON API under /api/v1 with full CRUD operations for posts, sources, and categories. All routes require authentication via Sanctum personal access tokens.
Read Endpoints (Phase 1):
/api/v1/posts– Paginated list of posts (default page size 20)- Filters (use JSON:API style bracket params):
filter[read]=0|1filter[source]={source_id}filter[category]={category_id}
- Includes:
include=source,category(comma separated) - Sorting:
sort=-posted_at(default newest first) orsort=posted_at - Pagination:
page(page number, starting at 1) andpage.size(1–200)
- Filters (use JSON:API style bracket params):
/api/v1/posts/{id}– Single post, supportsinclude=source,category/api/v1/posts/{id}/summary(POST) – Generate a short HTML summary/api/v1/sources– List sources, optionalinclude=category/api/v1/sources/{id}– Single source/api/v1/categories– List categories/api/v1/categories/{id}– Single category
Mutation Endpoints (Phase 2):
PATCH /api/v1/posts/{id}/read-status– Mark single post read/unreadPATCH /api/v1/posts/bulk-read-status– Mark multiple posts read/unread (up to 1000)PATCH /api/v1/posts/mark-all-read– Mark all posts read/unread with optional filtering
Management Endpoints (Phase 3):
POST /api/v1/sources– Create new RSS feed sourcePUT /api/v1/sources/{id}– Update existing sourceDELETE /api/v1/sources/{id}– Remove source and all postsPOST /api/v1/sources/{id}/refresh– Force refresh posts from sourceGET /api/v1/categories– List categories with source countsPOST /api/v1/categories– Create new categoryPUT /api/v1/categories/{id}– Update categoryDELETE /api/v1/categories/{id}– Remove categoryGET /api/v1/export-opml– Export all sources as OPMLPOST /api/v1/preview-opml– Preview OPML file before importPOST /api/v1/import-opml– Import sources from OPML file
See API-REFERENCE.md for complete documentation with examples, request/response formats, and error handling.
Response Shape (examples):
Posts list returns:
{
"data": [ { post fields ... } ],
"meta": { "total": n, "current_page": 1, "last_page": n, "per_page": 20 }
}
Summary endpoint returns:
{
"data": {
"post_id": 123,
"sentences": 2,
"summary": "<p>Sentence one.</p><p>Sentence two.</p>"
}
}
Errors use standard HTTP status codes (401 unauthenticated, 404, 422 validation, 502 upstream summary failure).
All /api/v1/* routes are protected by auth:sanctum. For manual usage you create a personal access token and send it as a Bearer token.
Create (Tinker):
php artisan tinker
>>> use App\Models\User;
>>> $user = User::first(); // or create a dedicated API user
>>> $token = $user->createToken('dev')->plainTextToken;
>>> $token
"1|XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Copy the full string (including the leading numeric id before the pipe). It is only shown once.
Request example (curl):
curl -H "Accept: application/json" -H "Authorization: Bearer 1|XXXX..." http://infraread.test/api/v1/posts
Revoke a token by deleting it from personal_access_tokens (e.g. $user->tokens()->where('name','dev')->delete();).
Route: POST /api/v1/posts/{id}/summary
Body (JSON, optional):
{ "sentences": 2 }
If sentences omitted a sensible default is used. A per-minute rate limit is enforced (configured via the summaries rate limiter in RouteServiceProvider / RateLimiter::for('summaries', ...)). Failures from the upstream summarizer respond with 502 and { "error": "..." }.
Path: /api-tester (behind normal web auth session). Paste a personal access token into the Auth Token box; subsequent in-page fetches add the Authorization: Bearer header. This page is purely a development aid and should be removed or protected further for production deployments.
- ✅ Phase 1: Read-only API endpoints with authentication and summaries
- ✅ Phase 2: Post mutation endpoints (mark read/unread, bulk operations)
- ✅ Phase 3: Source/category management and OPML import/export
- 🚧 Phase 6: Enhanced background processing and performance monitoring (foundation complete)
- Future: Vue SPA extraction to separate repository
- Future: External read-it-later service integrations
- Future: Advanced observability and metrics endpoints
Phase 6 Foundation Complete:
- Enhanced feed processing with comprehensive error handling
- Source health monitoring with exponential backoff for failed feeds
- Structured exception handling with detailed context preservation
- Performance tracking and metrics collection
- Improved console commands with progress reporting
- Database optimization with strategic indexes
- Plugin system overhaul with structured configuration and comprehensive testing
- Comprehensive test coverage (132 tests passing)
API Maturity:
- Complete CRUD operations for all entities
- Robust error handling and validation
- Rate limiting and security measures
- Full OPML import/export functionality
- Comprehensive documentation and examples
These notes document the comprehensive API implementation through Phase 3 (August 2025) including read endpoints, mutations, source/category management, OPML import/export, and Phase 6 foundation work with enhanced feed processing, error handling, and performance monitoring.
M.I.T. do anything you want with the code as long as you provide attribution back and you and don’t hold me liable
Find me on Twitter
Laravel, VueJs, Tailwind Css, Will Vincent Feeds which builds on top of SimplePie



