A decentralized RSS reader built on the AT Protocol. Your subscriptions, reading progress, and shares are stored in your Personal Data Server (PDS), giving you full ownership and portability of your data.
- Decentralized Storage: All user data stored in your Bluesky PDS
- Social Sharing: See what articles people you follow are sharing
- Offline Support: PWA with IndexedDB for offline reading
- Background Sync: Changes sync automatically when back online
┌─────────────────┐ ┌─────────────────────────────────────────────┐
│ Svelte 5 PWA │────▶│ CLOUDFLARE WORKERS │
│ (Frontend) │ │ │
└────────┬────────┘ │ Routes: auth, feeds, items, reading, │
│ │ records, shares, social, discover │
IndexedDB │ │
(Offline) │ ┌─────────────────────────────────────┐ │
│ │ │ Durable Objects │ │
└─WebSocket───▶│ │ RealtimeHub (WebSocket server) │ │
│ │ JetstreamPoller (firehose events) │ │
│ │ FeedRefresher (RSS refresh) │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ D1 │
│ (Users, Sessions, Feeds, │
│ Shares, Feed Cache, etc.) │
└──────────────┬──────────────────────────────┘
│
┌──────────────┼──────────────┐
↓ ↓ ↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User's PDS │ │ Jetstream │ │ RSS Feeds │
│(AT Protocol)│ │ Firehose │ │ (External) │
└─────────────┘ └─────────────┘ └─────────────┘
- Frontend: SvelteKit + Svelte 5 (runes), Dexie.js (IndexedDB)
- Backend: Cloudflare Workers + D1 + Durable Objects
skyreader/
├── frontend/ # Svelte 5 PWA
│ ├── src/
│ │ ├── lib/
│ │ │ ├── components/ # UI components (Sidebar, ArticleCard, ShareCard, etc.)
│ │ │ ├── stores/ # Svelte 5 rune stores (auth, subscriptions,
│ │ │ │ # reading, social, shares, shareReading,
│ │ │ │ # preferences, sidebar, realtime, keyboard, sync)
│ │ │ ├── services/ # API client, Dexie DB, sync queue, realtime
│ │ │ └── types/ # TypeScript types
│ │ ├── routes/ # SvelteKit pages (/, /social, /starred,
│ │ │ # /discover, /settings, /auth/*)
│ │ └── service-worker.ts # PWA service worker
│ └── static/
│ └── manifest.json # PWA manifest
├── backend/ # Cloudflare Workers
│ ├── src/
│ │ ├── routes/ # API handlers (auth, feeds, items, reading,
│ │ │ # records, shares, social, discover)
│ │ ├── services/ # OAuth, feed parser, article content,
│ │ │ # rate limiting, PDS sync
│ │ └── durable-objects/ # RealtimeHub, JetstreamPoller, FeedRefresher
│ └── migrations/ # D1 SQL migrations (16 migrations)
└── lexicons/ # AT Protocol schemas
└── app/skyreader/
├── feed/ # subscription, readPosition
└── social/ # share, follow, shareReadPosition
- Node.js 18+
- Cloudflare account (free tier works)
- Bluesky account for testing
-
Install dependencies:
cd backend npm install -
Create Cloudflare D1 database:
npx wrangler d1 create skyreader
-
Update
wrangler.tomlwith the database ID from step 2 -
Run the database migration:
npx wrangler d1 execute skyreader --remote --file=migrations/0001_initial.sql
-
Deploy:
npx wrangler deploy
-
Install dependencies:
cd frontend npm install -
Create
.envwith your backend URL:VITE_API_URL=https://skyreader-api.YOUR_SUBDOMAIN.workers.dev -
Run development server:
npm run dev
-
Open
http://127.0.0.1:5173(must use IP, not localhost, for OAuth)
The app defines five custom record types under the app.skyreader namespace:
app.skyreader.feed.subscription: RSS feed subscriptionsapp.skyreader.feed.readPosition: Read/starred state for articlesapp.skyreader.social.share: Shared articles with optional notesapp.skyreader.social.follow: In-app follow relationshipsapp.skyreader.social.shareReadPosition: Read state for others' shares
See Lexicon Documentation for detailed schema information.
- Subscriptions: Stored in user's PDS, cached locally in IndexedDB
- Reading Progress: Stored in PDS, synced via background queue
- Social Shares: Written to PDS, aggregated by backend via Jetstream firehose
The AT Protocol OAuth requires publicly accessible URLs. For local development:
- Deploy backend to Cloudflare Workers (free)
- Run frontend locally pointing to deployed backend
- Access frontend via
127.0.0.1(notlocalhost- RFC 8252 requirement)
# Backend
cd backend
npx wrangler dev # Local dev (limited - no real OAuth)
npx wrangler deploy # Deploy to Cloudflare
npx wrangler tail # Stream live logs
npx wrangler d1 execute skyreader --remote --command "SELECT * FROM users"
# Frontend
cd frontend
npm run dev # Development server
npm run build # Production build
npm run check # Type checkingGPL