ATCast - AT Protocol Podcast Host#
A podcast hosting platform that stores media files and RSS feeds using AT Protocol blob storage. Each user hosts their podcast on their own Bluesky/AT Protocol account - no centralized storage needed!
✨ Features#
- 🎙️ OAuth Authentication - Users log in with their own Bluesky account
- 📤 Large File Support - Automatic chunking for files over 5MB
- 📡 RSS Feed Generation - Standard RSS 2.0 feeds with AT Protocol extensions
- 🎵 Direct Streaming - Stream episodes from
at://URIs - 📝 Episode Management - Full CRUD operations for podcast episodes
- 🎨 Modern UI - React frontend with dark/light mode
- 🌐 Decentralized - Each user's podcast data stored on their own PDS
- 🔓 Open Access - Public RSS feeds accessible by DID
🚀 Quick Start: Fork & Deploy#
Option 1: Deploy to Railway (Recommended)#
- Click the Deploy button or go to Railway
- Fork this repository to your GitHub account
- Create a new project in Railway from your forked repo
- Set environment variables (see below)
- Deploy! Railway will automatically build and start your app
Required Environment Variables for Railway:
NODE_ENV=production
SESSION_SECRET=your-random-secret-key-here
PUBLIC_URL=https://your-app.up.railway.app
Option 2: Deploy to Render#
- Fork this repository to your GitHub account
- Go to Render Dashboard
- New → Web Service
- Connect your forked repository
- Configure:
- Build Command:
npm run build - Start Command:
npm start - Environment Variables: (see below)
- Build Command:
Required Environment Variables for Render:
NODE_ENV=production
SESSION_SECRET=your-random-secret-key-here
PUBLIC_URL=https://your-app.onrender.com
Option 3: Deploy to any Node.js host#
- Fork and clone this repository
- Install dependencies:
npm install - Build frontend:
npm run build - Set environment variables (create
.envfile) - Start:
npm start
🔐 Environment Variables#
| Variable | Description | Example |
|---|---|---|
PORT |
Server port | 5000 (default) |
NODE_ENV |
Environment | production or development |
SESSION_SECRET |
Secret for session encryption | Generate with openssl rand -base64 32 |
PUBLIC_URL |
Your app's public URL | https://your-app.railway.app |
Important:
- Generate a strong
SESSION_SECRET- never use the example value in production - Set
PUBLIC_URLto your actual deployed URL for OAuth to work correctly - No AT Protocol credentials needed - users authenticate with their own accounts!
📋 Prerequisites for Local Development#
- Node.js 18+
- A Bluesky/AT Protocol account (for testing)
🛠️ Local Development Setup#
1. Fork and Clone#
git clone https://github.com/YOUR-USERNAME/atproto-podcast-host.git
cd atproto-podcast-host
2. Install Dependencies#
npm install
This will install both backend and frontend dependencies automatically.
3. Configure Environment#
cp .env.example .env
Edit .env:
PORT=5000
NODE_ENV=development
SESSION_SECRET=your-random-secret-for-local-dev
PUBLIC_URL=http://localhost:5000
4. Run the Application#
Development Mode (runs both frontend and backend):
npm run dev:all
This starts:
- Backend API on
http://localhost:5000 - React frontend on
http://localhost:3000(proxies API requests to backend)
Or run separately:
# Terminal 1 - Backend only
npm run dev
# Terminal 2 - Frontend only
npm run client
Production Mode:
npm run build # Build React frontend
npm start # Start production server
The server serves both the React app and API on the same port.
🎯 How It Works#
- User logs in with their Bluesky handle via OAuth
- Upload episodes - Files are stored as blobs in the user's PDS
- Episodes stored as records - Metadata saved in
app.podcast.episodecollection - RSS feed generated - Standard RSS 2.0 with AT Protocol extensions
- Anyone can access - Public RSS feeds available via DID
📡 Key API Endpoints#
Authentication#
GET /api/auth/login?handle=user.bsky.social- Start OAuth loginGET /api/auth/callback- OAuth callback (automatic)GET /api/auth/session- Check current sessionPOST /api/auth/logout- Log out
Episodes#
POST /api/upload/episode- Upload new episode (multipart/form-data)GET /api/media/episodes- List all episodesGET /api/media/episodes/:id- Get specific episodePUT /api/media/episodes/:id- Update episode metadataDELETE /api/media/episodes/:id- Delete episodeGET /api/media/stream/:did/:cid- Stream episode audio
RSS Feed#
GET /api/feed/rss- Get RSS feed (authenticated)POST /api/feed/publish- Publish feed to AT ProtocolGET /api/feed/at-rss- Get published AT Protocol feedGET /api/feed/at-rss/:did- Get any user's feed by DID (public)GET /api/feed/uri- Get AT URI for published feed
Feed Settings#
GET /api/feed/metadata- Get podcast metadataPOST /api/feed/metadata- Update podcast metadata
💡 Usage Tips#
For Podcast Creators#
- Log in with your Bluesky handle at the deployed URL
- Set podcast metadata in Settings (title, description, etc.)
- Upload episodes - supports MP3, WAV, and other audio formats
- Publish RSS feed to AT Protocol for decentralized hosting
- Share your feed - Give listeners your AT URI or HTTP feed URL
For Podcast Listeners#
- HTTP Feed URL:
https://your-app.com/api/feed/at-rss/did:plc:creator-did - AT Protocol URI:
at://did:plc:creator-did/app.podcast.feed/self - Compatible with any podcast player that supports RSS feeds
Accessing Other Users' Feeds#
Anyone can access a published podcast feed without authentication:
# Get feed by DID
curl https://your-app.com/api/feed/at-rss/did:plc:user-did
# Stream episode
curl https://your-app.com/api/media/stream/did:plc:user-did/episode-cid
Project Structure#
atproto-podcast/
├── client/ # React Frontend (Vite)
│ ├── src/
│ │ ├── components/ # React components (Header, Sidebar, etc.)
│ │ ├── pages/ # Page components (Dashboard, Episodes, Settings)
│ │ ├── services/ # API service layer
│ │ ├── App.jsx # Main App component
│ │ └── main.jsx # Entry point
│ ├── public/ # Static assets
│ ├── package.json
│ └── vite.config.js # Vite configuration
├── src/ # Backend API (Express)
│ ├── index.js # Main server
│ ├── atproto-client.js # AT Protocol client
│ ├── routes/
│ │ ├── upload.js # Upload endpoints
│ │ ├── feed.js # RSS feed endpoints
│ │ └── media.js # Media streaming endpoints
│ └── storage/
│ └── metadata.js # Episode metadata storage
├── public/ # Legacy vanilla JS (deprecated)
│ ├── index.html
│ ├── app.js
│ └── styles.css
├── .env.example # Environment template
├── .gitignore
├── package.json
└── README.md
🏗️ Technical Architecture#
AT Protocol Integration#
Data Storage:
app.podcast.episode- Episode records with audio blob referencesapp.podcast.settings- Podcast metadata (title, description, etc.)app.podcast.feed- Published RSS feed XML
Blob Storage:
- Audio files stored as blobs in user's Personal Data Server (PDS)
- Files >5MB automatically chunked for PDS blob limits
- Each blob receives a unique CID (Content Identifier)
- Accessible via
at://URIs or HTTP streaming endpoint
Authentication:
- OAuth 2.0 flow with user's AT Protocol PDS
- No centralized credentials - each user authenticates independently
- Session management with file-based store (upgrade to Redis for production scale)
🔧 Technology Stack#
Backend:
- Express.js - Web server
- @atproto/oauth-client-node - OAuth authentication
- @atcute/client - AT Protocol XRPC operations
- @atproto/api - Public AT Protocol API access
- Multer - File upload handling
- RSS - Feed generation
Frontend:
- React 18 - UI framework
- Vite - Build tool and dev server
- React Router - Client-side routing
- Modern CSS with dark/light mode support
🌟 Why AT Protocol for Podcasting?#
- Decentralized Ownership - Your podcast data lives on your PDS, not a platform
- Portable - Move your podcast to any AT Protocol host
- Censorship Resistant - No central authority can remove your content
- Built-in Identity - Your Bluesky handle is your podcast identity
- Composable - Other apps can build on your podcast data
- No Storage Limits - Limited only by your PDS provider
📚 Additional Resources#
🤝 Contributing#
Contributions welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
🐛 Troubleshooting#
OAuth callback fails:
- Ensure
PUBLIC_URLmatches your actual deployment URL - Check that your app is accessible at that URL
- Verify session storage is working (check
sessions/directory)
Upload fails:
- Check file size (max 500MB, chunks at 5MB)
- Verify your PDS has available storage
- Check console logs for specific error messages
RSS feed not found:
- Publish the feed first using "Publish Feed" button in Settings
- Check that episodes exist before publishing
📄 License#
MIT License - see LICENSE file for details
⭐ Show Your Support#
If you find this project useful, please consider:
- Giving it a star on GitHub
- Sharing it with other podcast creators
- Contributing improvements
- Reporting bugs or suggesting features
Built with ❤️ for the AT Protocol ecosystem