Superseded by cloudflare-live-blog
This repository is the first attempt and is kept for reference. An updated take lives at Gryczka/cloudflare-live-blog (live demo), which rebuilds the same idea on Astro islands and SQLite-backed Durable Objects.
The rewrite exists because this version has real problems, and they are worth naming rather than quietly abandoning:
- The storage design cannot hold what the docs below claim. Every post is kept in a single key-value entry that is read, appended to, and rewritten on each publish. A key-value entry caps at 128KB, so this breaks at roughly a dozen full-length posts — not the 1,000 advertised further down this file.
- There is no authorization. Anyone who guesses a blog id can publish to it.
- Rate limiting is decorative — an in-memory
Mapinside the Durable Object, erased by every hibernation.- The reader downloads ~364KB of JavaScript and gets no post content in the server HTML, so nothing is visible until the bundle boots, hydrates, and fetches. (The markup also references a 112KB
polyfillschunk, but it isnoModule, so current browsers skip it.)npm run devruns a different application, because the dev-mode API routes are stubs that return fabricated data and refuse WebSocket upgrades.The rewrite's README covers each of these, plus the reconnect-storm and hydration bugs, and what replaced them. Its measured reader payload is 20,200 bytes.
The WebSocket hibernation walkthrough below and in CLOUDFLARE_CONCEPTS.md is still accurate and still a reasonable introduction to the primitives.
A real-time blogging platform powered by Cloudflare Durable Objects and WebSockets. Authors can write posts that are instantly broadcast to all connected readers without page refresh.
This project serves as an educational introduction to Cloudflare's developer platform, with comprehensive documentation, detailed code comments, and a complete Cloudflare Concepts Guide.
- Real-time Updates: Posts appear instantly on reader pages via WebSocket connections
- Durable Objects: Each blog backed by a single Cloudflare Durable Object for consistency
- WebSocket Hibernation: Memory-efficient WebSocket connections that can hibernate
- Next.js 15: Modern React with App Router and Server Components
- Cloudflare Workers: Deployed on Cloudflare's global edge network
- Tailwind CSS: Beautiful, responsive UI with dark mode support
- Educational Documentation: Comprehensive guides and inline code comments for learning
This project is an educational introduction to key Cloudflare Developer Platform concepts:
What are they? Durable Objects are stateful serverless objects that provide:
- Strong consistency: All requests for a given ID go to the same instance
- Persistent storage: Built-in key-value storage that survives instance restarts
- Global coordination: Perfect for managing shared state across multiple clients
In this project: Each blog ID maps to a single Durable Object instance. This ensures:
- All readers for a blog connect to the same DO
- Posts are stored reliably in DO storage
- WebSocket broadcasts reach all connected clients for that blog
Code location: src/durable-objects/LiveBlog.ts
What is it? WebSocket hibernation allows Durable Objects to:
- Accept WebSocket connections without staying in memory
- Automatically "wake up" when messages arrive
- Serialize connection state to survive hibernation
Benefits:
- Memory efficiency: Thousands of idle connections don't consume memory
- Cost savings: You only pay when the DO is actively processing
- Automatic scaling: Cloudflare handles the hibernation/wakeup lifecycle
In this project: Reader connections use hibernation:
// Accept WebSocket with hibernation support
this.ctx.acceptWebSocket(server);
// Serialize session data for hibernation
server.serializeAttachment({ id: sessionId });
// Auto-respond to pings without waking the DO
this.ctx.setWebSocketAutoResponse(
new WebSocketRequestResponsePair('ping', 'pong')
);When a new post is published, the DO wakes up, broadcasts to all connections, then hibernates again.
Code location: src/durable-objects/LiveBlog.ts:95-113
What is it? DOs can be identified by:
- Random IDs:
idFromString(crypto.randomUUID()) - Named IDs:
idFromName("my-blog")- deterministic mapping
In this project: We use named IDs:
const id = env.LIVEBLOG.idFromName(blogId);This ensures the blog ID "breaking-news" always maps to the same Durable Object instance, regardless of which Cloudflare data center handles the request.
Code location: src/worker/index.ts:76
How it works:
- Custom Worker is the entry point (src/worker/index.ts)
/api/liveblog/*requests → routed to Durable Objects- All other requests → passed to OpenNext (Next.js on Cloudflare)
This hybrid approach gives you:
- Full Next.js App Router features (SSR, Server Components)
- Direct access to Cloudflare primitives (Durable Objects, WebSockets)
- Optimal performance (no extra HTTP hop to reach DOs)
Code location: src/worker/index.ts:24-43
┌─────────────┐
│ Author │ Writes post
│ Interface │────────────┐
└─────────────┘ │
▼
POST /api/liveblog/{blogId}/atoms
│
▼
┌──────────────────────────────────────────┐
│ Custom Worker (src/worker/index.ts) │
│ - Intercepts /api/liveblog/* requests │
│ - Routes to correct Durable Object │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ LiveBlog Durable Object (Instance) │
│ - Stores atom in durable storage │
│ - Broadcasts to all WebSocket clients │
└──────────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
[Reader 1] [Reader 2] [Reader 3]
WebSocket WebSocket WebSocket
(hibernated) (hibernated) (hibernated)
│ │ │
▼ ▼ ▼
UI Updates UI Updates UI Updates
Instantly Instantly Instantly
-
LiveBlog Durable Object (src/durable-objects/LiveBlog.ts)
- Purpose: Manages state for a single blog
- Responsibilities:
- Stores blog posts (atoms) in durable storage
- Manages WebSocket connections with hibernation
- Broadcasts new posts to all connected readers
- Cloudflare Features: Durable Objects, WebSocket Hibernation, Durable Storage
-
Custom Worker Handler (src/worker/index.ts)
- Purpose: Entry point for all requests
- Responsibilities:
- Routes
/api/liveblog/*to Durable Objects - Passes other requests to OpenNext (Next.js)
- Exports the LiveBlog class for binding
- Routes
- Cloudflare Features: Workers, Durable Object Bindings
-
Reader UI (app/blog/[blogId]/page.tsx)
- Purpose: Display live blog posts to readers
- Responsibilities:
- Connects to WebSocket for live updates
- Displays all blog posts in real-time
- Auto-reconnects on disconnection
- Handles development vs. production mode
- Technologies: Next.js 15, React 19, WebSocket API
-
Author UI (app/blog/[blogId]/author/page.tsx)
- Purpose: Interface for creating posts
- Responsibilities:
- Form for writing and publishing posts
- Shows recent posts
- Provides instant feedback on publish
- Technologies: Next.js 15, React 19, Fetch API
live-blog/
├── app/ # Next.js App Router
│ ├── page.tsx # Landing page
│ ├── layout.tsx # Root layout
│ └── blog/[blogId]/
│ ├── page.tsx # Reader view
│ └── author/
│ └── page.tsx # Author view
├── src/
│ ├── durable-objects/
│ │ └── LiveBlog.ts # LiveBlog Durable Object
│ └── worker/
│ └── index.ts # Custom worker handler
├── wrangler.jsonc # Cloudflare Workers config
├── next.config.ts # Next.js config
└── open-next.config.ts # OpenNext config
- Node.js 18+
- npm or pnpm
- Cloudflare account (for deployment)
npm installThere are two ways to run the application locally:
npm run devThis starts the Next.js development server at http://localhost:3000
What works:
- ✅ UI development with hot reload
- ✅ Basic page navigation
- ✅ Mock API responses (no real-time updates)
What doesn't work:
- ❌ WebSocket real-time updates
- ❌ Durable Objects persistence
- ❌ Broadcasting to multiple readers
A yellow banner will appear on pages to indicate development mode limitations.
First, build the Next.js app:
npm run buildThen preview with Wrangler:
npm run previewThis starts a local Cloudflare Workers environment at http://localhost:8788
What works:
- ✅ Full WebSocket support
- ✅ Durable Objects with hibernation
- ✅ Real-time broadcasting
- ✅ Complete production-like behavior
Note: Changes require a rebuild (npm run build) - no hot reload.
Deploy to Cloudflare Workers:
npm run deployThis will:
- Build the Next.js application
- Generate OpenNext build for Cloudflare Workers
- Deploy to your Cloudflare account
- Visit the homepage
- Click "Create Random Blog" to generate a new blog with a random ID
- Or enter a specific blog ID and click "Go"
Visit /blog/{blogId} to:
- View all posts in the blog
- See real-time updates as authors publish new posts
- See connection status (connected/disconnected)
- Switch to Author Mode via the button
Visit /blog/{blogId}/author to:
- Write and publish new posts
- See recent posts
- View live statistics of published content
- Switch to Reader view to see how it looks
The API endpoints work differently depending on the runtime environment:
In Workers Runtime (npm run preview or production):
- Requests are intercepted by the custom worker at src/worker/index.ts
- Routes directly to the LiveBlog Durable Object
- Full WebSocket and real-time functionality
In Next.js Dev Mode (npm run dev):
- Handled by Next.js API Route Handlers at app/api/liveblog/
- Returns mock data or simulated responses
- WebSocket connections return 426 (Upgrade Required)
-
GET /api/liveblog/{blogId}/atoms- Get all posts- Workers: Fetches from Durable Object storage
- Dev Mode: Returns empty array with
_devModeflag
-
POST /api/liveblog/{blogId}/atoms- Create new post- Workers: Stores in DO and broadcasts to WebSockets
- Dev Mode: Returns mock atom without broadcasting
-
GET /api/liveblog/{blogId}/websocket- WebSocket upgrade- Workers: Upgrades to WebSocket connection
- Dev Mode: Returns 426 error with helpful message
-
GET /api/liveblog/{blogId}/metadata- Get blog metadata- Workers: Fetches from Durable Object
- Dev Mode: Not implemented (returns 404)
Currently, readers don't send messages (future: reactions, comments)
{
"type": "new_atom",
"atom": {
"id": "uuid",
"content": "The post content",
"timestamp": 1234567890,
"author": "Author Name"
}
}In wrangler.jsonc:
Important Notes:
- We don't specify
script_namebecause the Durable Object is defined in the same worker - Migrations are required when first creating a Durable Object. The
migrationssection tells Cloudflare you're adding a new DO class - The
tagcan be any string (commonly "v1", "v2", etc.) and is used to track schema changes - See Durable Objects Migrations for more details
The custom worker at src/worker/index.ts is configured as the main entry point.
This application implements multiple layers of security protection suitable for a public demo. All security features are production-ready but some are configured for demo mode and should be tightened for production use.
Server-Side Protection (src/durable-objects/LiveBlog.ts):
- Content sanitized using DOMPurify to prevent XSS attacks
- All HTML tags and attributes stripped from user input
- Length limits enforced: 10,000 chars for content, 100 chars for author names
- Blog IDs validated to alphanumeric + hyphens/underscores only
Client-Side Validation (app/blog/[blogId]/author/page.tsx):
- Real-time character count display
maxLengthattributes prevent exceeding limits- Visual feedback when approaching limits
- Validation before form submission
Post Creation (src/durable-objects/LiveBlog.ts:489-519):
- 10 posts per minute per IP address
- Sliding window rate limiting
- Returns 429 status with retry-after information
- Logged for audit purposes
WebSocket Connections (src/durable-objects/LiveBlog.ts:177-186):
- Maximum 1,000 active connections per blog
- Returns 503 when limit reached
- Prevents connection exhaustion attacks
Per-Blog Limits (src/durable-objects/LiveBlog.ts:250-262):
- Maximum 1,000 posts per blog
- Prevents storage exhaustion
- Returns 507 (Insufficient Storage) when limit reached
- Protects against DoS via storage abuse
Applied to All Responses (src/worker/index.ts:67-106):
Content-Security-Policy: Restricts script/style sources
X-Frame-Options: DENY - Prevents clickjacking
X-Content-Type-Options: nosniff - Prevents MIME sniffing
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: Disables camera, microphone, etc.
X-XSS-Protection: 1; mode=block - Legacy XSS protectionOrigin/Referer Validation (src/durable-objects/LiveBlog.ts:277-299):
- Validates Origin and Referer headers on POST requests
- Configurable whitelist for production enforcement
- Demo mode: logs attempts but allows (ENFORCE_ORIGIN_VALIDATION=false)
- Production mode: blocks invalid origins when enabled
Configurable Whitelist (src/durable-objects/LiveBlog.ts:14-19):
const ALLOWED_ORIGINS = [
'http://localhost:3000',
'http://localhost:8788',
'https://your-production-domain.com',
];- Demo mode: logs all origins but allows connections
- Production mode: enforces whitelist when ENFORCE_ORIGIN_VALIDATION=true
Security Event Logging (src/durable-objects/LiveBlog.ts:546-548):
- All security events logged with structured data
- Tracked events:
ATOM_CREATED- Post creation with IP and metricsRATE_LIMIT_EXCEEDED- Rate limit violationsSTORAGE_LIMIT_EXCEEDED- Storage quota hitsINVALID_CONTENT- Validation failuresWEBSOCKET_CONNECTION- WebSocket connections with originWEBSOCKET_LIMIT_EXCEEDED- Connection limit hitsWEBSOCKET_ORIGIN_REJECTED- Invalid origin attemptsCSRF_ATTEMPT_BLOCKED- CSRF attack attempts
- Includes IP address, origin, timestamps, and context
Information Disclosure Prevention:
- Generic error messages returned to clients
- Detailed errors logged server-side only
- Stack traces never exposed in responses
- Proper HTTP status codes used
Before deploying to production, update these settings:
In src/durable-objects/LiveBlog.ts:
const ENFORCE_ORIGIN_VALIDATION = true; // Change from false
const ALLOWED_ORIGINS = [
'https://your-production-domain.com', // Replace with your domain
'https://www.your-production-domain.com',
];In src/worker/index.ts, remove unsafe-eval if not needed:
"script-src 'self' 'unsafe-inline';" // Remove 'unsafe-eval' for productionTune rate limits based on your expected traffic:
const RATE_LIMIT_POSTS_PER_MINUTE = 10; // Adjust as needed
const MAX_WEBSOCKET_CONNECTIONS = 1000; // Adjust based on capacity- Configure Cloudflare Workers analytics
- Monitor security event logs
- Set up alerts for rate limit violations
- Track connection patterns
For production deployments, consider:
- Authentication system (OAuth, API keys, etc.)
- User accounts and permissions
- Content moderation tools
- Spam detection
- IP reputation checking
- DDoS protection tuning
| Resource | Limit | Configurable |
|---|---|---|
| Content Length | 10,000 characters | MAX_CONTENT_LENGTH |
| Author Name | 100 characters | MAX_AUTHOR_LENGTH |
| Blog ID Length | 100 characters | MAX_BLOG_ID_LENGTH |
| Posts Per Minute | 10 per IP | RATE_LIMIT_POSTS_PER_MINUTE |
| Posts Per Blog | 1,000 total | MAX_ATOMS_PER_BLOG |
| WebSocket Connections | 1,000 per blog | MAX_WEBSOCKET_CONNECTIONS |
As a public demo, this application intentionally:
- Has no authentication (anyone can post to any blog)
- Allows all origins in demo mode (controlled by flag)
- Has permissive rate limits for testing
- Does not implement user accounts or permissions
These are appropriate for a demo but should be addressed for production use.
The LiveBlog Durable Object uses WebSocket hibernation for memory efficiency:
this.ctx.acceptWebSocket(server);
server.serializeAttachment({ id: sessionId });When the Durable Object hibernates, WebSocket connections remain open but don't consume memory. When a message arrives, the DO is automatically reconstructed.
Posts are stored in Durable Object storage:
await this.ctx.storage.put('atoms', atoms);This provides strong consistency and automatic replication.
The custom worker intercepts /api/liveblog/* requests before they reach Next.js, routing them directly to the Durable Object for optimal performance.
npm run dev- Next.js development server (without Workers)npm run build- Build Next.js applicationnpm run preview- Preview with Wrangler locallynpm run deploy- Deploy to Cloudflarenpm run cf-typegen- Generate Cloudflare environment types
The application supports two development modes:
-
Next.js Dev Mode (
npm run dev)- Fast hot reload for UI development
- Mock API responses without real-time functionality
- Yellow banners indicate limited functionality
- Ideal for: UI/UX development, styling, layout work
-
Workers Preview Mode (
npm run preview)- Full Durable Objects and WebSocket support
- Production-like behavior locally
- Requires rebuild for changes
- Ideal for: Testing real-time features, integration testing
- Development Mode: WebSocket real-time updates don't work with
npm run dev. Usenpm run previewfor full testing. - Persistence: Data is stored in Durable Objects, which is persistent but not a traditional database. Data lives in memory and storage of the DO.
- Scaling: Each blog ID maps to a single Durable Object instance. This provides strong consistency but means all traffic for one blog goes to one instance.
- Hot Reload: Changes in Workers Preview mode require a full rebuild (
npm run build).
- Reader reactions (likes, emojis)
- Comment threads
- Markdown support for posts
- Image uploads
- Blog settings and customization
- Analytics and metrics
- Multiple authors per blog
- Post editing and deletion
- CLOUDFLARE_CONCEPTS.md - In-depth guide to Cloudflare concepts
- Durable Objects explained
- WebSocket hibernation deep dive
- Durable storage patterns
- Bindings and integration
- Best practices and common patterns
All source files include detailed educational comments explaining Cloudflare concepts:
- src/durable-objects/LiveBlog.ts - Durable Object implementation with WebSocket hibernation
- src/worker/index.ts - Custom worker routing and DO bindings
- Start here: Read the Cloudflare Concepts Explained section above
- Deep dive: Read CLOUDFLARE_CONCEPTS.md for comprehensive explanations
- Code along: Explore src/durable-objects/LiveBlog.ts with inline comments
- Build: Try modifying the code to add new features (see Future Enhancements)
- Next.js 15 - React framework with App Router
- React 19 - UI library
- Cloudflare Workers - Edge computing platform
- Cloudflare Durable Objects - Stateful serverless objects
- WebSockets - Real-time bidirectional communication
- OpenNext - Cloudflare adapter for Next.js
- TypeScript - Type-safe JavaScript
- Tailwind CSS - Utility-first CSS framework
MIT
Built with passion and the hope that lives literally depend on this. 🚀
{ "durable_objects": { "bindings": [ { "name": "LIVEBLOG", // The binding name (accessible as env.LIVEBLOG) "class_name": "LiveBlog" // The exported class name } ] }, "migrations": [ { "tag": "v1", // Migration version tag "new_classes": ["LiveBlog"] // New Durable Object classes being added } ] }