Move pixel state and rate-limit cooldowns out of Upstash Redis and into the existing CanvasRoom Durable Object's SQLite-backed storage. Worker becomes a thin validation/proxy; the DO does atomic cooldown check + pixel write + WS broadcast in one in-memory step. Why: eliminate external dependency, keep $0/month free-tier forever, exploit single-threaded actor for strong consistency without round-trips. Architecture: - canvas_chunks: 256 BLOB rows × 64 KB; CHUNK_COUNT derived from CANVAS_WIDTH × CANVAS_HEIGHT / CHUNK_BYTES so resize is config-only. - cooldowns: user_id → expires_at, 1% sample-rate lazy GC. - Worker forwards /api/canvas, /api/place, /api/ws to DO endpoints. - POST /admin/migrate-from-upstash: token-gated one-shot importer. Phases 1-3 code complete; Upstash dependency stays until prod migration runs and 7-day rollback window passes (Phase 4). Plan: plans/260509-2309-canvas-on-do-storage/
9.2 KiB
Research Report: Can rplace Be Moved to Vercel?
Date: 2026-05-09 22:46 (Asia/Saigon) Scope: Feasibility of migrating rplace (Cloudflare Workers + Durable Objects + Upstash) to Vercel. Verdict: Not a drop-in move. Requires architectural rewrite of the realtime layer.
Executive Summary
rplace cannot be lifted-and-shifted to Vercel. Two hard blockers:
- Cloudflare Durable Objects have no Vercel equivalent. rplace uses a DO (
CanvasRoom) as the single broadcast hub for all WebSocket clients — a stateful, globally-addressable actor. Vercel does not offer this primitive. - Vercel Functions cannot host WebSocket servers. Confirmed unchanged in 2026, even with Fluid Compute. Each invocation terminates after responding; no persistent process holds sockets open.
Everything else (Svelte SPA, Vite build, Upstash Redis storage, rate limiter) is portable. The realtime broadcast is the ~20% of the code that drives ~80% of the migration cost.
Recommended path if migration is mandatory: Vercel hosts the SPA + HTTP API; offload WS broadcast to a managed realtime provider (Ably / Pusher / Liveblocks / Partykit). Estimated effort: medium (1–3 days), plus a new monthly bill from the realtime provider.
Recommendation: Stay on Cloudflare unless there is a non-technical driver (org policy, billing consolidation). The current stack is a near-optimal fit for this workload; Vercel is a strict downgrade for realtime.
Methodology
- Sources: 2 web searches (Vercel WS support 2026, Vercel DO equivalent 2026)
- Code inspected:
src/worker.js,src/durable-objects/canvas-room.js,src/lib/redis-client.js,src/lib/canvas-storage.js,src/lib/rate-limiter.js,wrangler.json,package.json,README.md - Date: 2026-05-09
Cloudflare Coupling Inventory
| Component | File | Cloudflare Lock-in | Portable? |
|---|---|---|---|
| Worker entry (Hono) | src/worker.js |
Uses c.env, c.executionCtx.waitUntil |
Rewrite needed |
| Durable Object | src/durable-objects/canvas-room.js |
state.acceptWebSocket, Hibernation API, WebSocketPair, idFromName/get |
No equivalent |
| Redis client | src/lib/redis-client.js |
import { Redis } from '@upstash/redis/cloudflare' |
Trivial — swap to @upstash/redis |
| Canvas storage | src/lib/canvas-storage.js |
Upstash REST only | Yes |
| Rate limiter | src/lib/rate-limiter.js |
Upstash SET NX EX | Yes |
| Static assets | wrangler.json assets.directory |
CF static binding | Yes (Vercel serves SPA natively) |
| WS upgrade route | src/worker.js GET /api/ws |
Delegates to DO | No equivalent |
executionCtx.waitUntil |
src/worker.js |
CF runtime | Vercel has waitUntil via @vercel/functions |
Hard Blockers
1. Durable Objects (the showstopper)
CanvasRoom is the single broadcast room. All clients connect to the same DO instance (idFromName('main')) so a POST /api/place on any worker can reach every connected socket via one room.fetch('/broadcast') call. This is the entire architectural reason DOs exist.
Vercel has no actor / single-threaded stateful primitive. Confirmed 2026: "No equivalent exists on Vercel for Cloudflare Durable Objects." The official Vercel migration KB recommends external state (Redis) + third-party realtime services.
2. WebSocket Server Hosting
webSocketMessage / webSocketClose / webSocketError callbacks rely on Cloudflare's Hibernation API, which lets sockets survive worker eviction. Vercel Functions terminate per-request — they physically cannot keep a socket open across requests, even on Fluid Compute.
Migration Options (if forced)
Option A — Hybrid: Vercel + Managed Realtime (recommended if migrating)
Browser ──HTTP──▶ Vercel Function (Hono or Next API route)
│
├─▶ Upstash Redis (canvas + cooldown) — unchanged
└─▶ Ably/Pusher/Liveblocks/Partykit ──▶ broadcast to clients
Browser ◀──WS────────── (managed provider connection, not Vercel)
- Code changes: Replace
broadcastPixels()body withawait ably.channels.get('canvas').publish(...). Deletecanvas-room.js. Replace WS client connection URL. - New cost: ~$10–50/mo small tier (Ably/Pusher); Partykit free tier may suffice.
- Effort: ~1–3 days including testing.
- Risk: Two providers to monitor; broadcast no longer co-located with storage.
Option B — Vercel + SSE (no managed provider)
Replace WebSocket with Server-Sent Events + Redis Pub/Sub. Each client opens a long-lived SSE response from a Vercel Function. The function SUBSCRIBEs to a Redis channel and streams pixel events.
- Problem: Vercel Function max duration is bounded (Fluid Compute extends but is not infinite). Long-lived SSE streams burn function-seconds — billing concern at scale.
- Bidirectional? SSE is server→client only. rplace currently broadcasts only, so this is fine.
- Effort: ~2–4 days (more plumbing than Option A).
- Verdict: Cheaper monthly bill, more code to own.
Option C — Migrate to Cloudflare Pages instead
If the underlying motivation is "I want a Pages-like static + functions host," Cloudflare Pages with Functions + Durable Objects already does this and the code runs unchanged. Worth confirming the user's actual goal before assuming Vercel.
Option D — Rejected: Vercel-only with no realtime
Not viable. Polling GET /api/canvas (16 MB payload) every few seconds destroys the UX and bandwidth budget. Don't.
Cost / Effort Comparison
| Path | Effort | Monthly Cost Delta | Realtime Quality |
|---|---|---|---|
| Stay on Cloudflare | 0 | $0 | Excellent (current) |
| Vercel + Ably/Pusher | 1–3 days | +$10–50 | Excellent |
| Vercel + Partykit | 2–4 days | $0 (free tier) | Good (Partykit is DOs under the hood — ironic) |
| Vercel + SSE/Redis Pub/Sub | 2–4 days | Function-seconds at scale | Acceptable |
| Vercel polling-only | 0.5 day | Bandwidth $$$ | Unacceptable |
Things That Just Work on Vercel
- Svelte 5 + Vite SPA build → Vercel serves
dist/natively (novercel.jsonneeded for SPA). @upstash/redis(drop the/cloudflaresubpath).- HTTP API routes — Hono runs on Vercel Functions via
@hono/vercel. executionCtx.waitUntil→ useimport { waitUntil } from '@vercel/functions'.- IP-based
getUserId— Vercel exposes client IP viax-forwarded-for/x-real-ip.
Concrete Migration Steps (Option A, sketch)
npm i @hono/vercel @vercel/functions ably(or chosen provider).- Move
src/worker.js→api/[[...path]].js, export via@hono/verceladapter. - Replace
import { Redis } from '@upstash/redis/cloudflare'→'@upstash/redis'. - Delete
src/durable-objects/,wrangler.json,migrations. - Replace
broadcastPixels()to publish on Ably channelcanvas. - Update client
src/client/...to subscribe via Ably SDK instead ofnew WebSocket('/api/ws'). - Add
vercel.jsononly if SPA fallback routing needs tweaking. - Remove
wranglerfrom devDeps; addvercelCLI for local preview. - Move
wrangler secretenv vars → Vercel project env (Upstash creds + Ably key). - Run integration tests; the existing testcontainers Redis tests stay valid.
Recommendation
Don't migrate unless there is a business/ops reason. The current Cloudflare stack is the right tool — DOs solve exactly the problem (single-room WS broadcast with stateful coordination) that rplace has, in fewer moving parts than any Vercel-shaped alternative.
If migration is mandatory, Option A (Vercel + Ably or Partykit) is the lowest-risk path. Plan for ~3 days of work, a new vendor relationship, and minor monthly cost.
Unresolved Questions
- What is driving the migration request? (Cost? Org consolidation? Curiosity?) The right answer changes per motivation.
- Is Cloudflare Pages (Functions + DOs, Pages-style DX) acceptable as a middle ground?
- Acceptable monthly budget for a managed realtime provider vs. function-seconds for SSE?
- Are there latency requirements that rule out non-edge providers?
Sources
- Vercel Functions WebSocket Support (KB)
- Migrate to Vercel from Cloudflare (Vercel KB)
- Does Vercel Support WebSockets with Fluid Compute? (Vercel Community, 2025–2026)
- WebSockets on Vercel: Why Serverless Functions Can't Host Them (Ably)
- How We Built WebSocket Servers for Vercel Functions (Rivet, 2025-10)
- Cloudflare Durable Objects Overview
- Cloudflare Durable Objects vs Liveblocks Broadcast 2026 (Ably)
- Cloudflare Workers vs Vercel 2026 (Morph)