The Deployment Architecture
PriceGuess Israel isn't a single deployment -- it's three things working together. The Astro website runs on Cloudflare Pages. The 1v1 multiplayer game rooms run on a separate Cloudflare Worker with Durable Objects. And the daily scraper runs as a CRON-triggered Worker. Getting all three to play nicely together was the final puzzle.
Why Pages Can't Host Durable Objects
This was the most confusing part of Cloudflare's platform when I was starting out. Pages can run server-side code through its Functions feature, but it cannot host Durable Objects. DOs require a dedicated Worker deployment. So even though my Astro app needs to communicate with game rooms, the game rooms must live in a separate Worker.
The solution is service bindings. A service binding lets one Cloudflare service call another directly, without going through the public internet. My Astro app's API routes use a service binding called GAME_SERVICE to communicate with the DO Worker:
// wrangler.toml (Astro Pages config)
[[services]]
binding = "GAME_SERVICE"
service = "priceguess-1v1"
// In an API route
export async function POST({ request, env }: APIContext) {
const { roomId, action } = await request.json();
const id = env.GAME_SERVICE.idFromName(roomId);
const room = env.GAME_SERVICE.get(id);
return room.fetch(request);
}
The Deploy Script
Order matters when deploying. The DO Worker must exist before the Astro app can reference it via service binding. I created a single deploy command that handles everything in sequence:
// package.json
{
"scripts": {
"deploy:worker": "cd workers/1v1 && wrangler deploy",
"deploy:pages": "astro build && wrangler pages deploy dist",
"deploy:all": "npm run deploy:worker && npm run deploy:pages"
}
}
Running npm run deploy:all deploys the DO Worker first, then builds and deploys the Astro site. If either step fails, the script stops. One command, full deployment, correct order every time.
Custom Domain Setup
Cloudflare makes custom domains trivially easy when your DNS is already on Cloudflare. I pointed price.etiel.dev to my Pages deployment, and it was live within seconds. SSL is automatic, and the CDN caches static assets at every edge location worldwide.
Environment Variables
Managing secrets across multiple services required some care. The Astro app needs database bindings and OAuth credentials. The DO Worker needs its own D1 binding. The scraper Worker needs API keys. Each has its own wrangler config with its own secrets:
- Pages: Google OAuth credentials, D1 binding, service binding to DO Worker
- DO Worker: D1 binding for storing match results
- Scraper Worker: D1 binding, external API keys
Build-time variables (like PUBLIC_WS_URL) go in the .env file and are baked into the client bundle during astro build. Runtime secrets are set via wrangler secret put and never appear in code.
Staying Within the Free Tier
Cloudflare's free tier is generous, but you need to be aware of the limits:
- Workers: 100,000 requests/day -- more than enough for a daily game
- D1: 5GB storage, 5 million rows read/day -- plenty for product data and scores
- Durable Objects: 1 million requests/month on the free plan with SQLite classes
- Pages: Unlimited bandwidth, 500 builds/month
The key insight is using new_sqlite_classes instead of new_classes for Durable Objects on the free plan. This is poorly documented but critical. Without it, DO deployments simply fail on the free tier.
Monitoring and Debugging
Cloudflare's dashboard provides real-time logs for Workers, including WebSocket connections. I also added basic logging within the DO Worker to track game creation, player connections, and disconnections. When something goes wrong (and it will), having structured logs in the dashboard makes debugging possible without SSH access to any server.
Lessons Learned
Building PriceGuess taught me several things I'll carry into future projects:
- Start simple, layer complexity. The daily game was playable in two weeks. Multiplayer took another month. Auth and rankings took two more weeks. Each layer built on solid foundations.
- Cloudflare's free tier is incredibly capable. I'm running a full-stack real-time multiplayer game with authentication, a database, and scheduled tasks without paying a cent.
- Durable Objects are underrated. They solve the real-time stateful problem elegantly. Once I understood the mental model, building multiplayer was straightforward.
- Game feel is everything. The confetti, the animations, the share button -- these "non-essential" features are what make people come back.
What's Next
PriceGuess Israel is live at price.etiel.dev. I'm continuing to add products, tweak the ranking system, and explore new game modes. If you're in Israel (or just curious about Israeli grocery prices), give it a try. And if you have questions about anything in this series, feel free to reach out.
Thanks for reading the entire series. Building this project was one of the most rewarding experiences of my development career, and I hope these posts help someone else building something similar.