Gator is a command-line RSS feed aggregator built with TypeScript, PostgreSQL, and Drizzle ORM.
It allows multiple local users to add RSS feeds, follow feeds created by other users, continuously collect posts in the background, and browse the latest articles directly from the terminal.
The project was originally built as part of the Boot.dev Gator guided project and was later refactored into a cleaner layered architecture.
- Register local users
- Switch between existing users
- Add RSS feeds
- List feeds and their creators
- Follow and unfollow feeds
- Support many-to-many relationships between users and feeds
- Continuously fetch RSS feeds
- Fetch the least-recently-updated feed first
- Parse RSS XML
- Store posts in PostgreSQL
- Prevent duplicate posts using unique URLs
- Browse posts from feeds followed by the current user
- Configurable browse limits
- Configurable aggregation intervals
- Logged-in command middleware
- PostgreSQL migrations with Drizzle Kit
- Type-safe database access with Drizzle ORM
- TypeScript
- Node.js
- PostgreSQL
- Drizzle ORM
- Drizzle Kit
- postgres.js
- fast-xml-parser
- tsx
- NVM
Gator separates CLI logic, commands, database access, RSS parsing, and aggregation.
CLI
|
v
Command Registry
|
+------ Logged-In Middleware
|
v
Command Handlers
|
+------------+
| |
v v
Database RSS Service
Queries |
| v
v Remote RSS Feeds
PostgreSQL
The aggregator runs as a long-lived process.
Get next feed
|
v
Fetch RSS
|
v
Parse posts
|
v
Store new posts
|
v
Update last_fetched_at
|
v
Wait and repeat
Before running Gator, install:
- Git
- NVM
- Node.js
- npm
- PostgreSQL
The repository contains an .nvmrc, so NVM can automatically use the correct Node.js version.
git clone https://github.com/MhmdRayanm7/Blog-Aggregator.git
cd Blog-Aggregatornvm install
nvm usenpm installnpm run typecheckCreate a PostgreSQL database called:
gator
On Linux or WSL:
sudo -u postgres psqlThen:
CREATE DATABASE gator;Exit PostgreSQL:
\q
You can test the connection with:
psql "postgres://postgres:YOUR_PASSWORD@localhost:5432/gator"Replace YOUR_PASSWORD with your PostgreSQL password.
Your PostgreSQL username, password, port, or database name may be different depending on your local setup.
Gator stores its local configuration in:
~/.gatorconfig.json
Create the file:
touch ~/.gatorconfig.jsonAdd your PostgreSQL connection string:
{
"db_url": "postgres://postgres:YOUR_PASSWORD@localhost:5432/gator?sslmode=disable"
}Do not add current_user_name manually.
Gator automatically manages it when you register or log in.
An example config after using the application may look like:
{
"db_url": "postgres://postgres:YOUR_PASSWORD@localhost:5432/gator?sslmode=disable",
"current_user_name": "lane"
}Never commit database credentials to the repository. The Gator config belongs in your home directory, outside the project.
Apply the existing migrations:
npm run migrateWhen changing the database schema during development:
npm run generate
npm run migrateThe general command format is:
npm run start <command> [...arguments]For example:
npm run start register laneCreates a new user and makes that user current.
npm run start register laneSwitches to an existing user.
npm run start login laneLists all users and marks the current one.
npm run start usersExample:
* lane
* allan (current)
* hunter
Adds a new RSS feed.
The user who adds the feed automatically follows it.
npm run start addfeed "Hacker News" "https://news.ycombinator.com/rss"Lists every feed, its URL, and the user who created it.
npm run start feedsFollows an existing feed.
npm run start follow "https://news.ycombinator.com/rss"Stops following a feed.
npm run start unfollow "https://news.ycombinator.com/rss"Lists feeds followed by the current user.
npm run start followingStarts the long-running RSS aggregator.
npm run start agg 1mThe duration supports:
ms milliseconds
s seconds
m minutes
h hours
Examples:
npm run start agg 500ms
npm run start agg 30s
npm run start agg 1m
npm run start agg 1hGator immediately fetches one feed and then continues using the configured interval.
Stop it safely with:
Ctrl+C
You should normally leave the aggregator running in one terminal while using Gator from another terminal.
Displays recent posts from feeds followed by the current user.
npm run start browseThe default limit is:
2
Provide a custom limit:
npm run start browse 10Deletes the local users and related application data.
npm run start resetThis command is mainly useful during development and testing.
Create a clean local environment:
npm run start resetRegister a user:
npm run start register laneAdd Hacker News:
npm run start addfeed "Hacker News" "https://news.ycombinator.com/rss"Check followed feeds:
npm run start followingStart collecting posts:
npm run start agg 1mLeave that terminal running.
Open another terminal and browse collected posts:
npm run start browse 5Register another user:
npm run start register allanFollow the existing Hacker News feed:
npm run start follow "https://news.ycombinator.com/rss"Now both users can consume posts from the same feed without creating duplicate feed records.
Gator uses four main tables.
Stores Gator users.
Each username is unique.
users
├── id
├── created_at
├── updated_at
└── name
Stores RSS feeds.
Each feed has one creator and a unique URL.
feeds
├── id
├── created_at
├── updated_at
├── name
├── url
├── user_id
└── last_fetched_at
last_fetched_at is used by the aggregator to decide which feed should be fetched next.
Implements the many-to-many relationship between users and feeds.
feed_follows
├── id
├── created_at
├── updated_at
├── user_id
└── feed_id
A user can follow many feeds.
A feed can be followed by many users.
The same user/feed pair cannot be inserted twice.
Stores articles collected from RSS feeds.
posts
├── id
├── created_at
├── updated_at
├── title
├── url
├── description
├── published_at
└── feed_id
Post URLs are unique.
This means repeatedly fetching the same RSS feed does not create duplicate posts.
users
|
| creates
v
feeds
|
| has
v
posts
users
|
| follows
v
feed_follows
|
v
feeds
Or more formally:
users 1 ----< feeds
users >----< feeds
through
feed_follows
feeds 1 ----< posts
.
├── .nvmrc
├── drizzle.config.ts
├── package.json
├── tsconfig.json
│
└── src
├── index.ts
├── config.ts
├── rss.ts
├── aggregator.ts
│
├── cli
│ ├── middleware.ts
│ ├── registry.ts
│ └── types.ts
│
├── commands
│ ├── aggregate.ts
│ ├── auth.ts
│ ├── feeds.ts
│ ├── follows.ts
│ └── posts.ts
│
└── db
├── index.ts
├── schema.ts
│
├── migrations
│
└── queries
├── users.ts
├── feeds.ts
├── feedFollows.ts
└── posts.ts
Application entry point.
It registers the available commands and dispatches CLI arguments to the correct handler.
Contains the custom CLI infrastructure.
The project intentionally does not depend on a command framework such as Commander.
Contains command handlers grouped by feature.
auth.ts authentication and users
feeds.ts feed management
follows.ts following relationships
posts.ts post browsing
aggregate.ts long-running aggregation
Contains the PostgreSQL connection, Drizzle schema, migrations, and queries.
Database logic is kept out of command handlers whenever possible.
Fetches and parses external RSS documents.
Coordinates feed scheduling, RSS fetching, and post persistence.
Run the CLI:
npm run start <command>Type-check the project:
npm run typecheckGenerate a new Drizzle migration:
npm run generateApply migrations:
npm run migrateGator uses its own command registry instead of a CLI framework.
This keeps command dispatch explicit and demonstrates how CLI applications work internally.
Commands such as:
addfeed
follow
unfollow
following
browse
require an active user.
Instead of repeating user lookup logic inside every handler, Gator wraps these commands with logged-in middleware.
Database queries live separately from CLI command handlers.
This keeps the command layer focused on:
input
validation
orchestration
output
while the database layer handles persistence.
The aggregator selects feeds by last_fetched_at.
Feeds that have never been fetched are prioritized, followed by the least recently fetched feed.
This allows one long-running worker to rotate continuously through all feeds.
Feed URLs and post URLs are unique.
Posts are inserted using conflict-safe behavior, preventing repeated RSS downloads from creating duplicate articles.
After changing TypeScript code:
npm run typecheckAfter changing src/db/schema.ts:
npm run generate
npm run migrateFor manual database inspection:
psql "postgres://postgres:YOUR_PASSWORD@localhost:5432/gator"Useful PostgreSQL commands:
\dtSELECT * FROM users;
SELECT * FROM feeds;
SELECT * FROM feed_follows;
SELECT * FROM posts;Hacker News:
https://news.ycombinator.com/rss
TechCrunch:
https://techcrunch.com/feed/
Boot.dev:
https://www.boot.dev/blog
Not every website exposes RSS in exactly the same structure, so some feeds may require additional parser support.
Make sure the command exists and is registered in the CLI registry.
Example:
npm run start usersVerify PostgreSQL is running and confirm your connection string:
psql "postgres://postgres:YOUR_PASSWORD@localhost:5432/gator"Apply migrations:
npm run migrateMake sure:
- A user is logged in.
- The user follows at least one feed.
- The aggregator has collected posts.
Run:
npm run start followingThen start:
npm run start agg 10sAfter some feeds have been fetched:
npm run start browse 5