Podcasts hosted on ATProto
JavaScript 64.3%
CSS 28.8%
HTML 6.9%
Other 0.1%
40 1 0

Clone this repository

https://tangled.org/tigwyk.tv/atproto-podcast-host
git@tangled.org:tigwyk.tv/atproto-podcast-host

For self-hosted knots, clone URLs may differ based on your setup.

README.md

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#

Deploy on Railway

  1. Click the Deploy button or go to Railway
  2. Fork this repository to your GitHub account
  3. Create a new project in Railway from your forked repo
  4. Set environment variables (see below)
  5. 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#

  1. Fork this repository to your GitHub account
  2. Go to Render Dashboard
  3. NewWeb Service
  4. Connect your forked repository
  5. Configure:
    • Build Command: npm run build
    • Start Command: npm start
    • Environment Variables: (see below)

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#

  1. Fork and clone this repository
  2. Install dependencies: npm install
  3. Build frontend: npm run build
  4. Set environment variables (create .env file)
  5. 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_URL to 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#

  1. User logs in with their Bluesky handle via OAuth
  2. Upload episodes - Files are stored as blobs in the user's PDS
  3. Episodes stored as records - Metadata saved in app.podcast.episode collection
  4. RSS feed generated - Standard RSS 2.0 with AT Protocol extensions
  5. Anyone can access - Public RSS feeds available via DID

📡 Key API Endpoints#

Authentication#

  • GET /api/auth/login?handle=user.bsky.social - Start OAuth login
  • GET /api/auth/callback - OAuth callback (automatic)
  • GET /api/auth/session - Check current session
  • POST /api/auth/logout - Log out

Episodes#

  • POST /api/upload/episode - Upload new episode (multipart/form-data)
  • GET /api/media/episodes - List all episodes
  • GET /api/media/episodes/:id - Get specific episode
  • PUT /api/media/episodes/:id - Update episode metadata
  • DELETE /api/media/episodes/:id - Delete episode
  • GET /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 Protocol
  • GET /api/feed/at-rss - Get published AT Protocol feed
  • GET /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 metadata
  • POST /api/feed/metadata - Update podcast metadata

💡 Usage Tips#

For Podcast Creators#

  1. Log in with your Bluesky handle at the deployed URL
  2. Set podcast metadata in Settings (title, description, etc.)
  3. Upload episodes - supports MP3, WAV, and other audio formats
  4. Publish RSS feed to AT Protocol for decentralized hosting
  5. 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 references
  • app.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.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

🐛 Troubleshooting#

OAuth callback fails:

  • Ensure PUBLIC_URL matches 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