PHPackages                             theprivateer/subworthy - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. theprivateer/subworthy

ActiveProject[Mail &amp; Notifications](/categories/mail)

theprivateer/subworthy
======================

Subscribe to blogs, news sites and podcasts and get it all delivered to your inbox once a day in your own personalised newsletter.

121PHP

Since May 28Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/theprivateer/subworthy)[ Packagist](https://packagist.org/packages/theprivateer/subworthy)[ RSS](/packages/theprivateer-subworthy/feed)WikiDiscussions main Synced today

READMEChangelogDependenciesVersions (1)Used By (0)

Subworthy
=========

[](#subworthy)

Subscribe to blogs, news sites and podcasts and get it all delivered to your inbox once a day in your own personalised newsletter.

Requirements
------------

[](#requirements)

- PHP 8.2+
- Node.js (for frontend assets)
- A database supported by Laravel (SQLite, MySQL, PostgreSQL)
- [Laravel Herd](https://herd.laravel.com/) (recommended for local development)

Getting started
---------------

[](#getting-started)

```
composer install
npm install
cp .env.example .env
php artisan key:generate
php artisan migrate
npm run build
```

Development
-----------

[](#development)

The site is served automatically by Laravel Herd at `https://subworthy.test`.

To process feeds and send issues, both the scheduler and queue worker must be running:

```
php artisan schedule:work      # runs the scheduler every minute
php artisan queue:work         # processes queued jobs
```

To rebuild frontend assets:

```
npm run dev       # dev server with HMR
npm run build     # production build
```

Testing
-------

[](#testing)

```
php artisan test                        # full test suite
php artisan test --compact              # compact output
php artisan test --filter=ClassName     # single test class or method
```

How it works
------------

[](#how-it-works)

### Content pipeline

[](#content-pipeline)

Feed checking and issue delivery are entirely queue-driven. A scheduler fires every minute:

1. Feeds whose `next_check_at` time matches the current UTC minute are dispatched as `CheckFeed` jobs.
2. **`CheckFeed`** — imports the RSS/Atom feed via `laminas/laminas-feed`. Creates or updates `Post` records. If the feed has a `fetcher` class configured, dispatches `FetchFullPost` for each new post. Advances `next_check_at` by one hour on success, or 15 minutes on failure.
3. **`FetchFullPost`** — runs a custom `FetcherContract` implementation to scrape richer content (e.g. `ProducthuntFetcher` extracts data from the page's Next.js JSON). Stores result in `Post.fetched_raw`.
4. Users whose `delivery_time` (UTC) matches the current minute, and whose `days_of_week` includes today, receive a `CreateDailyIssue` job.
5. **`CreateDailyIssue`** — collects posts since `last_delivered_at`, runs them through `PostFilterService`, stores surviving post IDs as JSON in a new `Issue` record, then dispatches `EmailDailyIssue`.
6. **`EmailDailyIssue`** — hydrates the issue's posts and sends the `NewIssue` mail notification.

Daily maintenance jobs prune `Post` and `Issue` records older than one month (pruned posts are archived to `ArchivedPost` as tombstones), and remove feeds that have no remaining subscribers.

### Feed extensibility

[](#feed-extensibility)

Two fields on `Feed` allow per-feed customisation:

- `Feed.formatter` — fully-qualified class implementing `FormatterContract`. Defaults to `DefaultFormatter`, which sanitises HTML via HTMLPurifier, adds `target="_blank"` to links, and resolves relative image URLs.
- `Feed.fetcher` — fully-qualified class implementing `FetcherContract`. Run as `FetchFullPost` after `CheckFeed`. See `ProducthuntFetcher` for an example.

### Filtering

[](#filtering)

`PostFilterService` evaluates per-subscription `Filter` records against each post. A filter has `field`, `operator`, and `pattern`. The operator (e.g. `contains`, `does_not_contain`, `regex`) maps to a method via `_camelCase` dynamic dispatch. A matching filter returns `true`, which excludes the post from the issue.

### Delivery schedule

[](#delivery-schedule)

`User.delivery_time_local` (e.g. `0800`) combined with `User.timezone` is converted to a UTC `delivery_time` on save and stored as a 4-character string. The scheduler matches that string against the current UTC `Hi`-format time each minute. `days_of_week` is a string of ISO day numbers (1–7).

### Data model

[](#data-model)

ModelNotes`User`Auth, delivery schedule, timezone`Feed`Shared across users; holds RSS URL, optional formatter/fetcher class`Subscription`Joins User + Feed; optional title override; has many Filters`Post`Belongs to Feed; `raw` = original RSS HTML; `fetched_raw` = scraper output`Issue`Belongs to User; `posts` JSON column = included post IDs; `posts_excluded` = filtered-out IDs`ArchivedPost`Tombstone (feed\_id + source\_id) to prevent re-import after pruning`ReadLater`Joins User + Post for the read-later queue`Filter`Belongs to Subscription; field + operator + pattern### Public access

[](#public-access)

- `/@{username}` — public profile showing a user's issue archive
- `/issue/{issue}` — publicly viewable issue
- `/link/{user}/{post}` — link tracking redirect

Stack
-----

[](#stack)

LayerTechnologyFrameworkLaravel 13PHP8.2+FrontendBootstrap 5, Alpine.js (via Livewire), ViteReactive UILivewire 3Feed parsinglaminas/laminas-feedHTML sanitisationezyang/htmlpurifierHTTPGuzzle 7, Symfony BrowserKit/HttpClientSpam protectionspatie/laravel-honeypotTestingPHPUnit 12

###  Health Score

21

—

LowBetter than 18% of packages

Maintenance61

Regular maintenance activity

Popularity5

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/884249d2bb312969d25674c0f5b296ff7fc03776ea2dafabedc4d0b04c711acb?d=identicon)[theprivateer](/maintainers/theprivateer)

---

Top Contributors

[![theprivateer](https://avatars.githubusercontent.com/u/23644679?v=4)](https://github.com/theprivateer "theprivateer (25 commits)")

### Embed Badge

![Health badge](/badges/theprivateer-subworthy/health.svg)

```
[![Health](https://phpackages.com/badges/theprivateer-subworthy/health.svg)](https://phpackages.com/packages/theprivateer-subworthy)
```

###  Alternatives

[maize-tech/laravel-email-domain-rule

Laravel Email Domain Rule

612.0k](/packages/maize-tech-laravel-email-domain-rule)[sarfraznawaz2005/noty

Laravel package to incorporate noty flash notifications into laravel.

324.5k](/packages/sarfraznawaz2005-noty)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
