Cloudflare Containers
Deploy the Unbroken Protocol Bun server to Cloudflare Containers for real SSE and long-poll with persistent connections. Unlike Durable Objects, containers can maintain long-running connections without duration billing concerns.
When to Use Containers vs Durable Objects
| Aspect | Durable Objects | Containers |
|---|---|---|
| Connection model | Non-blocking (returns immediately) | Blocking (server waits) |
| SSE support | Sends once, closes | True streaming |
| Long-poll | Returns 204 immediately | Waits up to 30s |
Client pollInterval | Required | Not needed |
| Latency | Depends on poll interval | Real-time |
| Cost model | Per-request + duration | Container runtime |
| Best for | Low-cost, many streams | Real-time, fewer streams |
Choose Containers when:
- You need true real-time updates (< 100ms latency)
- You have fewer, high-activity streams
- You want standard SSE/long-poll behavior
Choose Durable Objects when:
- You have many low-activity streams
- Cost optimization is priority
- Eventual consistency (1-3s delay) is acceptable
Quick Start
1. Create Dockerfile
The @unbroken-protocol/server package includes a ready-to-use Dockerfile:
# packages/server/DockerfileFROM oven/bun:1.1-alpine AS baseWORKDIR /app
# Install dependenciesFROM base AS depsCOPY package.json bun.lock* ./RUN bun install --frozen-lockfile --production
# Build stageFROM base AS buildCOPY package.json bun.lock* ./RUN bun install --frozen-lockfileCOPY . .RUN bun run build
# Production stageFROM base AS productionRUN addgroup -g 1001 -S unbroken && \ adduser -S unbroken -u 1001RUN mkdir -p /app/data && chown -R unbroken:unbroken /app/dataCOPY --from=deps /app/node_modules ./node_modulesCOPY --from=build /app/dist ./distCOPY --from=build /app/src/main.ts ./src/main.tsCOPY package.json ./USER unbroken
ENV PORT=3000ENV HOST=0.0.0.0ENV DATA_DIR=/app/data
EXPOSE 3000HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["bun", "run", "src/main.ts"]2. Build and push image
# Build the imagedocker build -t unbroken-server ./packages/server
# Tag for your registrydocker tag unbroken-server registry.example.com/unbroken-server:latest
# Push to registrydocker push registry.example.com/unbroken-server:latest3. Deploy to Cloudflare Containers
# Install wrangler if needednpm install -g wrangler
# Login to Cloudflarewrangler login
# Deploy containerwrangler containers deploy \ --name unbroken-streams \ --image registry.example.com/unbroken-server:latest \ --port 3000Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
PORT | 3000 | Server port |
HOST | 0.0.0.0 | Server host |
DATA_DIR | ./data | Persistent storage directory |
LONG_POLL_TIMEOUT | 30000 | Long-poll timeout (ms) |
IDLE_TIMEOUT | 120 | Connection idle timeout (s) |
Persistent Storage
Mount a volume for data persistence:
wrangler containers deploy \ --name unbroken-streams \ --image registry.example.com/unbroken-server:latest \ --port 3000 \ --volume data:/app/dataClient Usage
With containers, you get true blocking behavior - no pollInterval needed:
import { stream } from "@unbroken-protocol/client"
// SSE - server keeps connection openconst response = await stream({ url: "https://unbroken-streams.example.com/events", offset: "-1", live: "sse", // No pollInterval needed - server streams in real-time})
for await (const chunk of response.jsonStream()) { console.log("Real-time data:", chunk)}// Long-poll - server waits for dataconst response = await stream({ url: "https://unbroken-streams.example.com/events", offset: "-1", live: "long-poll", // No pollInterval needed - server waits up to 30s})React Hook
import { useUnbrokenStream } from "@unbroken-protocol/react"
function RealtimeApp() { const { data, insert, isLoading } = useUnbrokenStream<Message>({ url: "https://unbroken-streams.example.com/messages", getKey: (msg) => msg.id, // No pollInterval - uses SSE with true streaming })
return <MessageList messages={data} />}Health Checks
The server exposes a health endpoint:
curl https://unbroken-streams.example.com/health# {"status":"ok"}Configure your container orchestrator to use this for liveness/readiness probes.
Scaling
Horizontal Scaling
For multiple container instances, use a shared storage backend:
# Each container mounts the same volumewrangler containers deploy \ --name unbroken-streams \ --replicas 3 \ --volume shared-data:/app/dataLoad Balancing
Cloudflare automatically load balances between container instances. SSE connections are sticky to ensure clients maintain their connection to the same instance.
Comparison: Full Stack
| Feature | DO Only | Containers Only | DO + Containers |
|---|---|---|---|
| Real-time SSE | ❌ | ✅ | ✅ |
| Global edge | ✅ | ❌ | ✅ (via DO) |
| Low cost at scale | ✅ | ❌ | ✅ |
| True long-poll | ❌ | ✅ | ✅ |
| Simple setup | ✅ | ✅ | ❌ |
For most use cases, choose one approach:
- Durable Objects: Cost-optimized, global, eventual consistency
- Containers: Real-time, persistent connections, standard behavior
Local Development
Run the server locally:
cd packages/server
# Install dependenciesbun install
# Start serverbun run start# or with custom configPORT=8080 DATA_DIR=./my-data bun run startOr with Docker:
docker build -t unbroken-server .docker run -p 3000:3000 -v $(pwd)/data:/app/data unbroken-serverNext Steps
- Durable Objects Deployment - Non-blocking, cost-optimized
- Protocol Overview - Understanding the protocol
- Reading Operations - SSE and long-poll modes