This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a monorepo with 4 packages:
backend/- Cloudflare Workers APIfrontend/- SvelteKit PWAadmin/- SvelteKit admin dashboard (Cloudflare Pages)feed-proxy/- Feed caching proxy (Fly.io)
Each package has its own CLAUDE.md with detailed guidance.
A decentralized RSS reader built on AT Protocol (Bluesky). User data (subscriptions, read state, shares) is stored in their Personal Data Server (PDS), making it portable. Features offline support, real-time updates, and social sharing of articles.
./scripts/dev-local.sh # Starts feed proxy, backend, and frontendThis script runs D1 migrations, then starts the feed proxy (port 3000), backend Wrangler dev server (port 8787), and frontend Vite dev server (port 5173). The frontend proxies /api requests to the backend via Vite, avoiding CORS issues.
Prerequisites:
- Bun installed (for the feed proxy)
- Install dependencies:
cd feed-proxy && bun install - Create
backend/.dev.varswith:FRONTEND_URL=http://127.0.0.1:5173 FEED_PROXY_URL=http://127.0.0.1:3000
Resetting the local database:
rm -rf backend/.wrangler/state/v3/d1/Note: Always access the frontend at http://127.0.0.1:5173 (not localhost) — OAuth requires 127.0.0.1.
Local development uses AT Protocol's localhost client exception, which allows OAuth without hosting client metadata or using tunnels.
npm run dev # Start Wrangler dev server (port 8787)
npm run test # Run vitest tests
npm run deploy # Deploy to Cloudflare Workers
npm run cf-typegen # Generate types from wrangler.toml
# Database
npx wrangler d1 execute skyreader --local --command "SQL"
npx wrangler d1 execute skyreader --local --file=migrations/FILE.sqlnpm run dev # Start Vite dev server (port 5173)
npm run build # Production build → build/
npm run check # Type checking (svelte-check)Note: Access the frontend at http://127.0.0.1:5173 for local development.
npm run dev # Start Vite dev server (port 5174)
npm run build # Production build → .svelte-kit/cloudflare/
npm run preview # Preview build locally with wrangler
npm run check # Type checking (svelte-check)
npm run deploy # Build and deploy to Cloudflare Pagesnpm run test:e2e # Run Playwright tests (starts servers automatically)
npm run test:e2e:ui # Interactive Playwright UI
npm run test:e2e:headed # Run with visible browserPlaywright spins up the backend (port 8787) and frontend (port 5173) via webServer config, or reuses already-running servers from ./scripts/dev-local.sh.
Prerequisites:
backend/.dev.varsmust includeALLOWED_ORIGINS=http://127.0.0.1:5173- Install:
npm install && npx playwright install chromium(from root)
Test structure:
e2e/
├── global-setup.ts # Applies D1 migrations before test run
├── seed.ts # Seeds test data into D1 via wrangler CLI
├── fixtures.ts # Custom Playwright fixtures (testUser, authedPage)
└── custom-fields.spec.ts # Test specs
Key patterns:
- Auth bypass:
seed.tsinserts users/sessions/settings directly into D1. TheauthedPagefixture sets thesession_idcookie andskyreader-authlocalStorage. - PDS disabled: Seeded
user_settingshaspds_sync_enabled=0so tests don't need a real PDS. - Valid TIDs: Seeded subscription rkeys must match
/^[a-z0-9]{13,}$/(AT Protocol TID format). - Async PATCH: The frontend fires subscription PATCH requests in the background. Use
page.waitForResponse()to ensure D1 is updated before reloading. - Cleanup: Each test's
testUserfixture automatically deletes its seeded data after the test.
Frontend (SvelteKit + Svelte 5) Admin (SvelteKit + Svelte 5)
↓↑ HTTP/REST ↓↑ D1 (read-only)
Backend (Cloudflare Workers) Cloudflare Pages
↓↑
AT Protocol (Bluesky PDS) + Fly.io Feed Proxy + Jetstream Firehose
- Runtime: Cloudflare Workers
- Database: D1 (SQLite) for all storage (sessions, feeds, shares, labels, etc.)
- Durable Objects: JetstreamPoller (long-running firehose connection via alarms)
- Auth: PKCE + DPoP OAuth flow
- Cron: Every 1 min (ping JetstreamPoller DO, cleanup rate limits), hourly (cleanup expired sessions/OAuth states)
- Framework: SvelteKit 2.x with Svelte 5 runes
- State: Rune stores in
.svelte.tsfiles (not writable stores) - Offline: IndexedDB via Dexie.js + sync queue
- PWA: Service worker for offline support
- Framework: SvelteKit 2.x with Svelte 5 runes
- Runtime: Cloudflare Pages
- Database: D1 (SQLite) - reads the same database as the backend
- Features: System metrics dashboard, user management, feed health monitoring, search/sort/pagination
- Pages: Dashboard (metrics overview), Users (list + detail), Feeds (health + error tracking)
- Deploy: Cloudflare Pages via GitHub Actions (staging on push to main, production on release)
- Auth: Handle → DID resolution → OAuth PKCE/DPoP → session token
- Subscriptions: Stored in user's PDS, cached locally in IndexedDB
- Feed Updates: Frontend requests feeds → backend proxies via Fly.io feed cache
- Social: Jetstream firehose → D1 shares table → frontend polls for updates
Custom lexicons in lexicons/app/skyreader/:
feed/subscription.json- RSS subscription recordfeed/saved.json- Saved article recordsocial/share.json- Shared article recordsocial/shareReadPosition.json- Share read position
Records are synced bidirectionally between the app and user's PDS.