Ship It — Deploying to Cloudflare Workers (Part 6/6)
We made it. Five parts of building, encrypting, testing, and polishing. Now let’s put this thing on the internet.
Deploying a full-stack application — SPA frontend plus edge Worker backend — in a single command sounds too good to be true. With Cloudflare Workers and Wrangler, it actually is that simple. Let me show you the whole pipeline from zero to live.
What we’re deploying
Let’s recap the moving parts:
- SPA (React + Vite) → built to
/dist, served as static assets - Worker (Hono) → thin Upstash auth proxy with SSE streaming
- Secrets → Upstash REST URL and token, stored in Cloudflare’s secret store
- Database → Upstash Redis (hosted separately, free tier)
The goal: one wrangler deploy command that builds the SPA, bundles the Worker, uploads both, and serves them from the same origin.
Step 1: Set up Upstash Redis
Head to upstash.com and create a free Redis database. You’ll need two pieces:
- REST URL —
https://your-db.upstash.io - Token — a long alphanumeric string
The free tier gives you 10,000 commands per day. For a planning poker app, that’s enough for a team doing several sprint plannings a week. Each room lifecycle (create, join, vote, reveal, leave) uses roughly 10-20 commands per player.
Step 2: Store secrets in Cloudflare
Secrets are injected at runtime — they never appear in your code, your config files, or your git history:
npx wrangler secret put UPSTASH_URL
# paste your URL when prompted
npx wrangler secret put UPSTASH_TOKEN
# paste your token when prompted
These are stored encrypted in Cloudflare’s infrastructure and are only available to your Worker at runtime via env.UPSTASH_URL and env.UPSTASH_TOKEN. They don’t exist in wrangler.jsonc, they don’t exist in .env, they don’t exist anywhere a human or a CI pipeline can accidentally expose them.
Step 3: Understand the wrangler config
{
"name": "planning-poker",
"main": "worker/src/index.ts",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "./dist",
"not_found_handling": "single-page-application",
"binding": "ASSETS"
},
"observability": { "enabled": true },
"build": {
"command": "npm install --prefix worker && pnpm build"
}
}
Let’s break this down:
main: "worker/src/index.ts" — The Worker entry point. Wrangler bundles this with esbuild before deploying.
assets.directory: "./dist" — The Vite build output. These files get uploaded to Cloudflare’s edge and served by the ASSETS binding.
assets.not_found_handling: "single-page-application" — This tells Cloudflare: “If a request doesn’t match a static file, serve index.html.” Critical for React Router — without this, navigating to /planning-poker/room/abc123 directly would 404.
assets.binding: "ASSETS" — Makes the static assets available as a Cloudflare binding. The Worker can call env.ASSETS.fetch(req) to programmatically serve static files (which it does for the SPA fallback and cache control).
build.command — Runs automatically before wrangler deploy. It installs the Worker’s own dependencies (npm install --prefix worker) and builds the SPA (pnpm build). One command does everything.
observability.enabled: true — Turns on Cloudflare’s built-in Worker analytics. Request counts, error rates, latency — without adding any instrumentation code.
Step 4: The build pipeline
When wrangler deploy runs, here’s what happens:
1. wrangler runs the build command:
npm install --prefix worker → installs Hono + hono/cors in worker/
pnpm build → tsc + vite build → /dist
2. wrangler bundles worker/src/index.ts with esbuild
→ single JS file for the Worker
3. wrangler uploads:
→ Worker bundle → Cloudflare edge (global)
→ /dist/* assets → Cloudflare edge (global)
4. The Worker is live at:
https://planning-poker.oltionzefi.workers.dev
The pnpm build step runs tsc && vite build — TypeScript type-checking first (catch errors before they ship), then Vite’s production build with tree-shaking, minification, and code-splitting.
Step 5: Deploy
npx wrangler deploy
That’s it. Seriously. One command. The output looks something like:
🌀 Building...
✨ Compiled Worker successfully
📦 Published planning-poker (2.3 seconds)
planning-poker.oltionzefi.workers.dev
The app is live. Open the URL. Create a room. Share the link. Estimate some stories.
Local development flow
For day-to-day development, you don’t deploy to Cloudflare. You run locally with direct Upstash access:
# .env (git-ignored)
VITE_UPSTASH_URL=https://your-db.upstash.io
VITE_UPSTASH_TOKEN=your-token
VITE_DEBUG_LOGGING=true
pnpm dev
This starts Vite’s dev server at http://localhost:5173. The adapter auto-detects the VITE_UPSTASH_* variables and switches to direct mode — the browser talks to Upstash directly, no Worker needed.
The VITE_DEBUG_LOGGING=true flag enables the logger utility, giving you detailed console output for every SSE message, state update, and encryption operation. Super helpful for debugging real-time sync issues.
Environment variable summary
Here’s the complete picture:
| Variable | Where | Purpose |
|---|---|---|
VITE_UPSTASH_URL | .env (local dev only) | Direct Upstash REST URL |
VITE_UPSTASH_TOKEN | .env (local dev only) | Direct Upstash token |
VITE_WORKER_URL | .env (optional) | Override Worker base URL |
VITE_BASE_URL | .env (optional) | Base path (default /) |
VITE_DEBUG_LOGGING | .env (dev only) | Enable verbose console output |
UPSTASH_URL | wrangler secret | Upstash REST URL for the Worker |
UPSTASH_TOKEN | wrangler secret | Upstash token for the Worker |
Production has zero VITE_* variables. The SPA detects same-origin deployment and routes everything through /upstash. Clean.
Keeping things clean: the pre-commit workflow
Before deploying, I always run:
pnpm format # Prettier — consistent code style
pnpm lint # ESLint — zero warnings
pnpm test # Vitest — all tests pass
pnpm build # TypeScript + Vite — no type errors
And if you push to GitHub, the CI pipeline (ci.yml) runs format:check, lint, and test automatically on every push and PR. Nothing broken merges to main.
Dependabot
The project uses GitHub’s Dependabot to keep dependencies fresh:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
Weekly PRs for outdated packages. Merge the ones that pass CI, review the ones that don’t. Low effort, big payoff for security patches.
Cost breakdown
This entire stack costs $0 for a small team:
| Service | Free Tier | Enough for… |
|---|---|---|
| Cloudflare Workers | 100K requests/day | ~50 sprint plannings/day |
| Upstash Redis | 10K commands/day | ~10 concurrent rooms |
| GitHub Actions | 2K minutes/month | ~200 CI runs/month |
No credit card required for any of these. Seriously.
If you outgrow the free tier (congrats!), Cloudflare Workers Paid is $5/month for 10M requests. Upstash Pay-as-you-go is $0.2 per 100K commands. Still basically free.
What we built — the full picture
Let’s zoom out and appreciate what we’ve created over these 6 parts:
🃏 A real-time planning poker app with Fibonacci, T-Shirt, Powers of 2, and custom decks
🔐 End-to-end encrypted with AES-GCM-256 — the server literally cannot read your data
⚡ Instant updates via SSE streaming through a Cloudflare Worker
🎭 Smart roles — facilitator, voter, observer, with automatic handoff
⏱ Countdown timer with auto-reveal and preset chips
📊 Live results with vote distribution charts and consensus detection
📜 Session history — every round archived in your browser session
🔗 One-click sharing — encryption key embedded in the URL hash
🧪 Tested — Vitest + Testing Library with 80% coverage thresholds
🚀 Deployed — one wrangler deploy to 300+ edge locations worldwide
🆓 Free — no accounts, no sign-up, no cost
The entire codebase is about 2,500 lines of TypeScript across the SPA and Worker. No server-side business logic. No database migrations. No user management. Just a browser, some crypto, and a thin pipe to Redis.
What I’d add next
If I were to keep building:
- Custom timer durations — let the facilitator type an arbitrary number of seconds
- Spectator count — show how many observers are watching
- Export session history — download as CSV or JSON
- Sound effects — a subtle ding when all votes are in 🔔
- Custom themes — let teams pick their accent color
But honestly? It works. The team uses it. The data is private. And it costs nothing.
Try it yourself
The app is live at planning-poker.oltionzefi.workers.dev.
Open it. Create a room. Share the link. Estimate some stories. If it works for your team — and I think it will — you just got a free, encrypted, real-time planning poker tool. No strings attached.
Keep pushing forward and savor every step of your coding journey.
