Skip to content

feat: migrate from Node.js/PostgreSQL to Cloudflare Workers/D1#140

Merged
mahata merged 3 commits intomainfrom
feat/cloudflare-workers-migration
Mar 26, 2026
Merged

feat: migrate from Node.js/PostgreSQL to Cloudflare Workers/D1#140
mahata merged 3 commits intomainfrom
feat/cloudflare-workers-migration

Conversation

@mahata
Copy link
Copy Markdown
Owner

@mahata mahata commented Mar 26, 2026

Summary

Migrate the entire runtime and database stack from Node.js + PostgreSQL to Cloudflare Workers + D1 (SQLite).

  • Runtime: Node.js (@hono/node-server) → Cloudflare Workers (wrangler dev)
  • Database: PostgreSQL (pg + drizzle-orm/node-postgres) → D1 SQLite (drizzle-orm/d1)
  • WebSockets: @hono/node-wshono/cloudflare-workers upgradeWebSocket
  • Static assets: @hono/node-server/serve-static → Wrangler [assets] config with public/ directory
  • Environment: process.env / dotenvc.env bindings (.dev.vars for secrets, wrangler.toml [vars] for non-secrets)

Key Changes

Database (Phase 1)

  • Schema converted from PostgreSQL types (pgTable, serial, text, timestamp) to SQLite types (sqliteTable, integer().primaryKey({autoIncrement}), text, integer for timestamps)
  • Global db singleton replaced with getDb(c.env.DB) factory that creates a drizzle instance per-request from the D1 binding
  • PostgreSQL migrations replaced with a single SQLite migration (0000_certain_omega_red.sql)
  • Added seed.sql for seeding the #general channel
  • Removed docker-compose.yml and PostgreSQL connection config

Runtime (Phase 2)

  • Entry point (hono/index.ts) simplified to export default app (Workers convention)
  • Session middleware wrapped in a lazy initializer since hono-sessions resolves the encryption key at creation time, but Workers only provide c.env at request time
  • Google OAuth credentials passed explicitly from c.env via createMiddleware wrapper (avoids module-level process.env reads)
  • WebSocket onOpen never fires on CF Workers — client registration moved to lazy init in onMessage
  • D1 binding captured eagerly during WebSocket upgrade (context unavailable after 101 response)
  • nodejs_compat flag enables node:crypto (scrypt, randomBytes, timingSafeEqual) — no changes needed in password hashing

Dependencies

Removed: @hono/node-server, @hono/node-ws, ws, @types/ws, dotenv, pg, @types/pg
Added: wrangler, @cloudflare/workers-types

Configuration

  • wrangler.toml: Worker main, assets config, D1 database binding, compatibility flags
  • .dev.vars: Local dev secrets (gitignored)
  • tsconfig.json / tsconfig.prod.json: Added @cloudflare/workers-types
  • playwright.config.ts: Updated webServer command for wrangler dev

Known Issues

  • "The script will never generate a response" on WebSocket close: Known Miniflare bug (cloudflare/workers-sdk#5433). Cosmetic only — occurs in local dev, not in production, no functional impact.
  • Stderr noise in index.test.tsx: Error auto-joining #general appears because the mock D1 binding doesn't implement prepare(). Tests still pass — the auto-join is wrapped in try/catch.

Verification

All 3 CI gates pass:

  • Tests: 61 passed (11 test files)
  • Lint: Clean (Biome)
  • Build: 0 type errors (tsc --noEmit), client + assets + server all build successfully
  • Manual: Verified with wrangler dev — login, chat, WebSocket messaging, channel switching all work

Migrate the entire runtime and database stack:

- Runtime: Node.js + @hono/node-server → Cloudflare Workers (wrangler dev)
- Database: PostgreSQL (pg) + drizzle-orm/node-postgres → D1 (SQLite) + drizzle-orm/d1
- WebSockets: @hono/node-ws → hono/cloudflare-workers upgradeWebSocket
- Static assets: @hono/node-server/serve-static → Wrangler [assets] config
- Environment: process.env/dotenv → c.env bindings (.dev.vars + wrangler.toml [vars])
- Session: Lazy middleware wrapper to resolve SESSION_SECRET from c.env at request time
- Auth: Google OAuth credentials read from c.env via createMiddleware wrapper
- Schema: pgTable → sqliteTable, serial → integer().primaryKey({autoIncrement}), etc.
- DB access: Global db singleton → getDb(c.env.DB) factory per request

Removed: docker-compose.yml, PostgreSQL migrations, pg/dotenv/@hono/node-server deps
Added: wrangler.toml, .dev.vars, SQLite migration, seed.sql, wrangler/@cloudflare/workers-types deps
Copilot AI review requested due to automatic review settings March 26, 2026 13:41
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

Migrate the app runtime from Node.js to Cloudflare Workers (Wrangler) and the database from Postgres to Cloudflare D1 (SQLite), updating routing, WebSockets, and local dev/test tooling accordingly.

Changes:

  • Add Workers/D1 configuration (wrangler.toml) and switch Drizzle to drizzle-orm/d1 with SQLite schema + new migration/seed.
  • Update server entrypoint, WebSocket route, and app/session initialization patterns for Workers c.env bindings.
  • Revise build/dev scripts and test mocks to accommodate Workers bindings and asset serving via public/.

Reviewed changes

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

Show a summary per file
File Description
wrangler.toml New Workers + assets + D1 binding config
vitest.setup.ts Remove Postgres/OAuth env setup; keep NODE_ENV
tsconfig.prod.json Add Workers types for prod compilation
tsconfig.json Add Workers types for dev/test typing
pnpm-lock.yaml Dependency graph updates for Wrangler/Workers types
playwright.config.ts Run e2e against wrangler dev
package.json Switch scripts to wrangler + D1 migration/seed + asset build
hono/types.ts Add Workers Bindings; adjust createdAt type
hono/testApp.ts Test app now stubs c.env bindings (DB/NODE_ENV/secret)
hono/static/ChatPage.ts Add JSON typings for API/WS payload parsing
hono/routes/ws.ts Use D1-backed getDb(c.env.DB) and adjust WS handling
hono/routes/ws.test.ts Mock getDb and add env.DB to WS context
hono/routes/testAuth.ts Use c.env instead of process.env for dev-only test auth
hono/routes/messages.ts Use per-request D1 drizzle instance
hono/routes/messages.test.ts Mock getDb; update createdAt to string
hono/routes/index.tsx Use D1 drizzle instance for auto-join logic
hono/routes/index.test.tsx Update Hono env typing to include Bindings
hono/routes/emailAuth.ts Use D1 drizzle instance for user lookup/create
hono/routes/emailAuth.test.ts Mock getDb instead of singleton db
hono/routes/channels.ts Use D1 drizzle instance across channel handlers
hono/routes/channels.test.ts Mock getDb; update createdAt to string + typed JSON
hono/routes/auth.ts Create Google OAuth middleware using c.env bindings
hono/index.ts Workers entrypoint: export default app
hono/db/seed.sql New seed for general channel
hono/db/schema.ts Convert schema to sqliteTable + SQLite defaults/indexes
hono/db/migrations/meta/_journal.json Switch migrations metadata to sqlite + new baseline
hono/db/migrations/meta/0000_snapshot.json New sqlite snapshot for schema
hono/db/migrations/0000_certain_omega_red.sql New single sqlite migration
hono/db/migrations/0004_spotty_christian_walker.sql Remove old Postgres migration
hono/db/migrations/0003_narrow_maggott.sql Remove old Postgres migration
hono/db/migrations/0002_bent_maria_hill.sql Remove old Postgres migration
hono/db/migrations/0001_nosy_rawhide_kid.sql Remove old Postgres migration
hono/db/migrations/0000_oval_moon_knight.sql Remove old Postgres migration
hono/db/migrations/meta/0004_snapshot.json Remove old Postgres snapshot
hono/db/migrations/meta/0003_snapshot.json Remove old Postgres snapshot
hono/db/migrations/meta/0002_snapshot.json Remove old Postgres snapshot
hono/db/migrations/meta/0001_snapshot.json Remove old Postgres snapshot
hono/db/index.ts Replace Postgres pool singleton with D1 getDb()
hono/db/config.ts Remove Postgres connection config
drizzle.config.ts Switch Drizzle Kit dialect to sqlite
docker-entrypoint-initdb.d/create-test-db.sql Remove Postgres test DB init
docker-compose.yml Remove Postgres container setup
.gitignore Ignore public/, .dev.vars, .wrangler/
.env.sample Update instructions for .dev.vars + Workers env names
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread hono/routes/ws.ts
Comment on lines +29 to +36
onMessage: async (evt: MessageEvent<WSMessageReceive>, ws: WSContext) => {
if (!user) {
return;
}

if (!clients.has(ws)) {
clients.set(ws, { userEmail: user.email });
}
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.

If onOpen doesn't reliably run on Cloudflare Workers, an unauthenticated client will never be closed: onMessage just returns when !user, leaving the socket open. Close the socket with an auth error (e.g. 1008) when user is missing, and consider doing client registration/auth checks in the same place to avoid relying on onOpen.

Copilot uses AI. Check for mistakes.
Comment thread package.json
Comment on lines +8 to +12
"dev": "pnpm run build:assets && wrangler dev",
"build": "rm -rf dist public && pnpm run build:client && pnpm run build:assets && tsc --project tsconfig.prod.json",
"build:client": "tsc --project tsconfig.client.json",
"build:client:watch": "tsc --project tsconfig.client.json --watch",
"build:assets": "mkdir -p public/components public/static && cp hono/components/*.css public/components/ && cp hono/static/*.js hono/static/*.js.map public/static/",
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.

pnpm dev runs build:assets without running build:client, but build:assets copies hono/static/*.js and *.js.map which won't exist on a clean checkout (and cp will fail when globs don't match). Update dev to build/watch the client first (e.g. run build:client or build:client:watch alongside wrangler dev) and/or make build:assets tolerant of missing globs.

Copilot uses AI. Check for mistakes.
Comment thread wrangler.toml
Comment on lines +16 to +17
[vars]
NODE_ENV = "development"
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.

[vars] NODE_ENV = "development" in wrangler.toml applies to all environments unless overridden, which would make production run with NODE_ENV=development and unintentionally enable dev-only routes (e.g. /test/login, /debug/session). Move this into an env-specific section (e.g. [env.development.vars]) and ensure production sets NODE_ENV = "production" (or remove it entirely and rely on explicit env config).

Copilot uses AI. Check for mistakes.
Comment thread hono/app.tsx
Comment on lines +28 to 33
encryptionKey: c.env.SESSION_SECRET || "your-super-secret-key-change-in-production",
expireAfterSeconds: 3600,
cookieOptions: {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
secure: c.env.NODE_ENV === "production",
sameSite: "lax",
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.

SESSION_SECRET falls back to a hard-coded default encryption key. If this ever runs outside local dev/tests (or if secrets are misconfigured), sessions become forgeable. Prefer failing fast when c.env.SESSION_SECRET is missing (at least when NODE_ENV === "production") rather than using a default.

See below for a potential fix:

      const isProduction = c.env.NODE_ENV === "production";
      const sessionSecret = c.env.SESSION_SECRET;

      if (!sessionSecret && isProduction) {
        throw new Error("SESSION_SECRET must be set in production");
      }

      let encryptionKey = sessionSecret;

      if (!encryptionKey) {
        const randomBytes = new Uint8Array(32);
        crypto.getRandomValues(randomBytes);
        encryptionKey = btoa(String.fromCharCode(...randomBytes));
      }

      const store = new CookieStore();
      cached = sessionMiddleware({
        store,
        encryptionKey,
        expireAfterSeconds: 3600,
        cookieOptions: {
          httpOnly: true,
          secure: isProduction,

Copilot uses AI. Check for mistakes.
Comment thread hono/app.tsx
Comment on lines 61 to +63
app.route("/", index);
app.route("/", createWsRoute(upgradeWebSocket, clients));
app.route("/", testAuth);
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.

testAuth is now always registered. With the current wrangler.toml setting NODE_ENV="development", this would expose the E2E login/logout endpoints in production. Consider registering testAuth only when the request env indicates development (e.g. mount a guard middleware for /test/* that returns 404 unless c.env.NODE_ENV === "development") and ensure production vars cannot accidentally enable it.

Copilot uses AI. Check for mistakes.
Copilot AI and others added 2 commits March 26, 2026 23:32
)

* Initial plan

* fix: pass E2E_GMAIL_ACCOUNT as wrangler var to fix e2e tests in CI

Agent-Logs-Url: https://github.com/mahata/mlack/sessions/18390311-cf2c-400d-aaca-02ae58821737

Co-authored-by: mahata <23497+mahata@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mahata <23497+mahata@users.noreply.github.com>
)

* Initial plan

* fix: add db seed step in CI and fix WebSocket close handshake

Agent-Logs-Url: https://github.com/mahata/mlack/sessions/f1a795bc-88ea-44a2-b7ee-cec0e9b1ba1e

Co-authored-by: mahata <23497+mahata@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mahata <23497+mahata@users.noreply.github.com>
@mahata mahata merged commit 90a06b5 into main Mar 26, 2026
3 checks passed
@mahata mahata deleted the feat/cloudflare-workers-migration branch March 26, 2026 15:24
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