Skip to content

feat: add Durable Objects for WebSocket, production deploy support, update README#143

Merged
mahata merged 4 commits intomainfrom
feat/cloudflare-production-deploy
Mar 26, 2026
Merged

feat: add Durable Objects for WebSocket, production deploy support, update README#143
mahata merged 4 commits intomainfrom
feat/cloudflare-production-deploy

Conversation

@mahata
Copy link
Copy Markdown
Owner

@mahata mahata commented Mar 26, 2026

Summary

  • Implement a ChatRoom Durable Object using the WebSocket Hibernation API so real-time messaging works reliably across Cloudflare Worker isolates
  • Rewrite the /ws route as a thin auth-validating proxy that forwards WebSocket upgrades to the Durable Object
  • Add production deployment infrastructure: deploy, db:migrate:prod, db:seed:prod scripts and updated wrangler.toml with DO bindings and migrations
  • Fix the E2E CI job by removing the stale PostgreSQL service container and using wrangler D1 local commands instead
  • Delete the stale render.yaml (referenced a non-existent start:prod script)
  • Rewrite README.md to accurately describe the Cloudflare Workers/D1/Durable Objects stack and add a step-by-step deployment guide

Key Changes

Durable Object (hono/durableObjects/ChatRoom.ts)

Single ChatRoom instance manages all WebSocket connections with hibernation support. On message: saves to D1 via Drizzle, looks up channel members, broadcasts to connected members. User identity is persisted through hibernation via serializeAttachment/deserializeAttachment.

WebSocket Route (hono/routes/ws.ts)

Simplified from 111 to 31 lines. Validates session auth, attaches user info as URL params, forwards the upgrade request to env.CHAT_ROOM.getByName("main").fetch().

Configuration

  • wrangler.toml: Added [[durable_objects.bindings]] and [[migrations]] for ChatRoom, set NODE_ENV = "production"
  • package.json: Added deploy, db:migrate:prod, db:seed:prod scripts
  • hono/types.ts: Added CHAT_ROOM: DurableObjectNamespace to Bindings

CI

  • Removed PostgreSQL service container from E2E job
  • E2E now uses pnpm db:migrate / pnpm db:seed (wrangler D1 local)

Cleanup

  • Deleted render.yaml
  • Updated README with current tech stack, deployment guide, and accurate project structure

Post-merge: First Deploy Steps

After merging, the first production deployment requires:

  1. npx wrangler login
  2. npx wrangler d1 create mlack-db → update database_id in wrangler.toml
  3. pnpm db:migrate:prod and pnpm db:seed:prod
  4. npx wrangler secret put SESSION_SECRET (and GOOGLE_ID, GOOGLE_SECRET, GOOGLE_REDIRECT_URI)
  5. pnpm deploy

…pdate README

- Implement ChatRoom Durable Object with WebSocket Hibernation API for
  reliable real-time messaging across Worker isolates
- Rewrite /ws route as thin proxy that forwards upgrades to the DO
- Add deploy, db:migrate:prod, db:seed:prod scripts to package.json
- Update wrangler.toml with DO bindings, migrations, and production vars
- Fix E2E CI job: remove stale PostgreSQL service, use wrangler D1 local
- Delete stale render.yaml (Render.com config with non-existent scripts)
- Rewrite README.md to reflect Cloudflare Workers/D1/Durable Objects stack
  and add step-by-step Cloudflare deployment instructions
Copilot AI review requested due to automatic review settings March 26, 2026 21:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Cloudflare Durable Objects–backed WebSocket handling (with hibernation) and updates deployment/CI/docs to reflect a Workers + D1 stack.

Changes:

  • Introduce a ChatRoom Durable Object and refactor /ws into an auth-gated proxy forwarding upgrades to the DO.
  • Add production-oriented scripts/config (Wrangler DO bindings/migrations, deploy + remote D1 migrate/seed) and remove stale Render config.
  • Update CI and README to use Wrangler D1 local tooling and document Cloudflare deployment steps.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
wrangler.toml Adds DO binding + migration config; changes default NODE_ENV.
render.yaml Removes obsolete Render deployment configuration.
package.json Adds deploy and remote D1 migrate/seed scripts.
hono/types.ts Adds CHAT_ROOM binding type.
hono/testApp.ts Extends test env bindings to include CHAT_ROOM.
hono/routes/ws.ts Replaces in-process WS handling with DO-forwarding upgrade proxy.
hono/routes/ws.test.ts Updates unit tests to match new /ws route shape.
hono/index.ts Exports ChatRoom for Wrangler entrypoint usage.
hono/durableObjects/ChatRoom.ts New DO implementing WS message persistence + broadcast via D1/Drizzle.
hono/deployment.test.ts Adds mock for cloudflare:workers and checks DO module importability.
hono/app.tsx Routes /ws through the new proxy route.
README.md Rewrites docs to match Workers/D1/DO stack and adds deployment guide.
.github/workflows/ci.yml Removes Postgres service/env and uses Wrangler D1 local migrate/seed for E2E.

Comment thread hono/routes/ws.ts
Comment thread hono/routes/ws.ts
Comment on lines +22 to 25
const url = new URL(c.req.url);
url.searchParams.set("userEmail", user.email);
url.searchParams.set("userName", user.name);

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User identity is forwarded to the Durable Object via URL query parameters (userEmail, userName). This puts PII into request URLs (which are commonly logged/cached/visible in analytics) and can leak via referrers in some contexts. Prefer passing identity via headers (e.g., a signed/opaque token) or via the WebSocket attachment established server-side after the DO accepts the socket, rather than in the URL.

See below for a potential fix:

    const headers = new Headers(c.req.raw.headers);
    headers.set("x-user-email", user.email);
    headers.set("x-user-name", user.name);

    const doRequest = new Request(c.req.raw, { headers });

Copilot uses AI. Check for mistakes.
Comment thread hono/routes/ws.test.ts
Comment on lines +12 to 17
it("should return 426 for non-WebSocket requests", async () => {
const wsRoute = createWsRoute();

const response = await wsRoute.request("/ws");
expect(response.status).toBeGreaterThanOrEqual(400);
expect(response.status).toBe(426);
});
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated tests only cover route construction and the 426 response for non-WebSocket requests. They no longer cover the key new behavior: rejecting unauthenticated upgrade requests (401) and forwarding authenticated upgrades to env.CHAT_ROOM.getByName("main").fetch() with the expected identity propagation. Adding unit coverage with a mocked DurableObjectNamespace/stub would help prevent regressions.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit fcfad02. Added two new test cases to ws.test.ts:

  • 401 for unauthenticated upgrade: sends Upgrade: websocket headers without an authenticated user → expects 401
  • Forwarding with user identity: sends a WebSocket upgrade as an authenticated user, then asserts CHAT_ROOM.getByName("main") was called and the forwarded request URL contains the correct userEmail and userName query params

A createWsApp helper was introduced to mock both the session middleware and the DurableObjectNamespace binding inline, following the same pattern used in channels.test.ts and messages.test.ts.

Comment thread hono/deployment.test.ts
Comment thread wrangler.toml
tag = "v1"
new_sqlite_classes = ["ChatRoom"]

[vars]
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NODE_ENV is set to "production" in the default [vars], which makes local wrangler dev and Playwright’s webServer run with production-only behavior (notably secure: true session cookies in hono/app.tsx, so cookies won’t be set over http://localhost). Consider keeping the default as development (or omitting it) and moving NODE_ENV = "production" under an [env.production.vars] block (or overriding via --var NODE_ENV:production in the deploy command).

Suggested change
[vars]
[vars]
NODE_ENV = "development"
[env.production.vars]

Copilot uses AI. Check for mistakes.
mahata added 2 commits March 27, 2026 06:10
…mand

The process.env NODE_ENV set by Playwright's webServer.env does not
override wrangler.toml [vars]. Use --var NODE_ENV:development so the
/test/login endpoint allows mock authentication during E2E tests.
…WebSocket

The wrangler dev Durable Object WebSocket Hibernation API has a quirk
where client-initiated ws.close() never completes the close handshake,
so the browser onclose event never fires. Use Playwright's
page.routeWebSocket() to proxy and close the connection at the network
layer instead. Also remove redundant ws.close() from ChatRoom's
webSocketClose handler.
@mahata mahata merged commit dc81c7a into main Mar 26, 2026
3 checks passed
@mahata mahata deleted the feat/cloudflare-production-deploy branch March 26, 2026 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants