-
Notifications
You must be signed in to change notification settings - Fork 0
feat: migrate from Node.js/PostgreSQL to Cloudflare Workers/D1 #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,11 @@ | ||
| PORT=3000 | ||
| GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com | ||
| GOOGLE_CLIENT_SECRET=GOCSPX-xxx | ||
| GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google | ||
| SESSION_SECRET=your-super-secret-key-change-in-production | ||
|
|
||
| # Database Configuration | ||
| POSTGRES_HOST=localhost | ||
| POSTGRES_PORT=5437 | ||
| POSTGRES_USER=postgres | ||
| POSTGRES_PASSWORD=mysecretpassword | ||
| POSTGRES_DB=postgres | ||
| # Wrangler dev reads secrets from .dev.vars (not .env) | ||
| # Copy this file to .dev.vars and fill in values | ||
|
|
||
| SESSION_SECRET=your-super-secret-key-change-in-production | ||
| GOOGLE_ID=xxx.apps.googleusercontent.com | ||
| GOOGLE_SECRET=GOCSPX-xxx | ||
| GOOGLE_REDIRECT_URI=http://localhost:8787/auth/google | ||
| E2E_GMAIL_ACCOUNT=your-testing-account@gmail.com | ||
| E2E_GMAIL_PASSWORD=your-password-here | ||
|
|
||
| # E2E tests read from .env (Playwright config) | ||
| # E2E_GMAIL_PASSWORD=your-password-here |
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,7 @@ | ||
| import { defineConfig } from "drizzle-kit"; | ||
| import { dbCredentials } from "./hono/db/config.ts"; | ||
|
|
||
| export default defineConfig({ | ||
| schema: "./hono/db/schema.ts", | ||
| out: "./hono/db/migrations", | ||
| dialect: "postgresql", | ||
| dbCredentials, | ||
| dialect: "sqlite", | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,5 @@ | ||
| import { serveStatic } from "@hono/node-server/serve-static"; | ||
| import { createNodeWebSocket } from "@hono/node-ws"; | ||
| import { Hono } from "hono"; | ||
| import { upgradeWebSocket } from "hono/cloudflare-workers"; | ||
| import { csrf } from "hono/csrf"; | ||
| import type { MiddlewareHandler } from "hono/types"; | ||
| import type { WSContext } from "hono/ws"; | ||
|
|
@@ -13,66 +12,59 @@ import { index } from "./routes/index.js"; | |
| import { messagesRoute } from "./routes/messages.js"; | ||
| import { testAuth } from "./routes/testAuth.js"; | ||
| import { createWsRoute } from "./routes/ws.js"; | ||
| import type { Variables } from "./types.js"; | ||
| import type { Bindings, Variables } from "./types.js"; | ||
|
|
||
| type AppOptions = { | ||
| sessionMiddleware?: MiddlewareHandler<{ Variables: Variables }>; | ||
| sessionMiddleware?: MiddlewareHandler<{ Bindings: Bindings; Variables: Variables }>; | ||
| }; | ||
|
|
||
| export function createApp(options?: AppOptions) { | ||
| const app = new Hono<{ Variables: Variables }>(); | ||
|
|
||
| // Set up session middleware | ||
| if (options?.sessionMiddleware) { | ||
| app.use("*", options.sessionMiddleware); | ||
| } else { | ||
| const store = new CookieStore(); | ||
| app.use( | ||
| "*", | ||
| sessionMiddleware({ | ||
| function createLazySessionMiddleware(): MiddlewareHandler<{ Bindings: Bindings; Variables: Variables }> { | ||
| let cached: MiddlewareHandler | null = null; | ||
| return async (c, next) => { | ||
| if (!cached) { | ||
| const store = new CookieStore(); | ||
| cached = sessionMiddleware({ | ||
| store, | ||
| encryptionKey: process.env.SESSION_SECRET || "your-super-secret-key-change-in-production", | ||
| 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", | ||
| path: "/", | ||
| }, | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| // Create WebSocket helper | ||
| const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }); | ||
| }); | ||
| } | ||
| const mw = cached as MiddlewareHandler; | ||
| return mw(c, next); | ||
| }; | ||
| } | ||
|
|
||
| // Store connected WebSocket clients mapped to their user info | ||
| const clients = new Map<WSContext, { userEmail: string }>(); | ||
| export function createApp(options?: AppOptions) { | ||
| const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); | ||
|
|
||
| // Serve static files from components directory | ||
| app.use("/components/*", serveStatic({ root: "./hono" })); | ||
| if (options?.sessionMiddleware) { | ||
| app.use("*", options.sessionMiddleware); | ||
| } else { | ||
| app.use("*", createLazySessionMiddleware()); | ||
| } | ||
|
|
||
| // Serve static files from static directory | ||
| app.use("/static/*", serveStatic({ root: "./hono" })); | ||
| const clients = new Map<WSContext, { userEmail: string }>(); | ||
|
|
||
| app.use("*", csrf()); | ||
|
|
||
| // Register route handlers | ||
| app.route("/", health); | ||
| app.route("/", auth); | ||
| app.route("/", emailAuth); | ||
| app.route("/", channelsRoute); | ||
| app.route("/", messagesRoute); | ||
| app.route("/", index); | ||
| app.route("/", createWsRoute(upgradeWebSocket, clients)); | ||
| app.route("/", testAuth); | ||
|
Comment on lines
61
to
+63
|
||
|
|
||
| if (process.env.NODE_ENV === "development") { | ||
| app.route("/", testAuth); | ||
| } | ||
|
|
||
| return { app, injectWebSocket }; | ||
| return { app }; | ||
| } | ||
|
|
||
| const { app, injectWebSocket } = createApp(); | ||
| const { app } = createApp(); | ||
|
|
||
| export { app, injectWebSocket }; | ||
| export { app }; | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,16 @@ | ||
| import { drizzle } from "drizzle-orm/node-postgres"; | ||
| import { Pool } from "pg"; | ||
| import { dbCredentials } from "./config.js"; | ||
| import { drizzle } from "drizzle-orm/d1"; | ||
| import * as schema from "./schema.js"; | ||
|
|
||
| const pool = new Pool(dbCredentials); | ||
| let cachedDb: ReturnType<typeof drizzle<typeof schema>> | null = null; | ||
| let cachedD1: D1Database | null = null; | ||
|
|
||
| export const db = drizzle(pool, { schema }); | ||
| export function getDb(d1: D1Database) { | ||
| if (cachedDb && cachedD1 === d1) { | ||
| return cachedDb; | ||
| } | ||
| cachedD1 = d1; | ||
| cachedDb = drizzle(d1, { schema }); | ||
| return cachedDb; | ||
| } | ||
|
|
||
| export * from "./schema.js"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| CREATE TABLE `channel_members` ( | ||
| `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, | ||
| `channel_id` integer NOT NULL, | ||
| `user_email` text NOT NULL, | ||
| `joined_at` text DEFAULT (CURRENT_TIMESTAMP), | ||
| FOREIGN KEY (`channel_id`) REFERENCES `channels`(`id`) ON UPDATE no action ON DELETE no action | ||
| ); | ||
| --> statement-breakpoint | ||
| CREATE UNIQUE INDEX `channel_members_channel_id_user_email_unique` ON `channel_members` (`channel_id`,`user_email`);--> statement-breakpoint | ||
| CREATE TABLE `channels` ( | ||
| `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, | ||
| `name` text NOT NULL, | ||
| `created_by_email` text NOT NULL, | ||
| `created_at` text DEFAULT (CURRENT_TIMESTAMP) | ||
| ); | ||
| --> statement-breakpoint | ||
| CREATE UNIQUE INDEX `channels_name_unique` ON `channels` (`name`);--> statement-breakpoint | ||
| CREATE TABLE `messages` ( | ||
| `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, | ||
| `content` text NOT NULL, | ||
| `user_email` text NOT NULL, | ||
| `user_name` text, | ||
| `channel_id` integer NOT NULL, | ||
| `created_at` text DEFAULT (CURRENT_TIMESTAMP), | ||
| FOREIGN KEY (`channel_id`) REFERENCES `channels`(`id`) ON UPDATE no action ON DELETE no action | ||
| ); | ||
| --> statement-breakpoint | ||
| CREATE INDEX `messages_created_at_idx` ON `messages` (`created_at`);--> statement-breakpoint | ||
| CREATE INDEX `messages_channel_id_created_at_idx` ON `messages` (`channel_id`,`created_at`);--> statement-breakpoint | ||
| CREATE TABLE `users` ( | ||
| `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, | ||
| `email` text NOT NULL, | ||
| `name` text NOT NULL, | ||
| `password_hash` text NOT NULL, | ||
| `created_at` text DEFAULT (CURRENT_TIMESTAMP) | ||
| ); | ||
| --> statement-breakpoint | ||
| CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`); |
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SESSION_SECRETfalls 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 whenc.env.SESSION_SECRETis missing (at least whenNODE_ENV === "production") rather than using a default.See below for a potential fix: