- Description
- Deployed Application
- The Challenge
- Cloudflare Workers Setup
- Architecture Notes
- Contributors
An online book club platform catering to the horror community. Create posts, leave comments, and discover all new stories from a community of horror book lovers.
Originally deployed on Heroku; this branch migrates the app to Cloudflare Workers + D1 (see below). Update this link once you deploy your own copy with wrangler deploy.
AS A bookworm that loves the horror genre I WANT a place online to discuss books SO THAT I can find new books to read while always gaining deeper understanding of my favorite books
This app runs on Cloudflare Workers with a D1 (SQLite) database, R2 for image
uploads, and static assets served straight from public/. Everything is
plain JavaScript (no build step beyond precompiling the existing Handlebars
views — see Architecture Notes).
npm install
wrangler d1 create killer-reads-db
This prints a database_id. Copy it into wrangler.toml, replacing the
REPLACE_WITH_YOUR_DATABASE_ID placeholder under [[d1_databases]].
wrangler r2 bucket create killer-reads-images
wrangler secret put SESSION_SECRET
wrangler secret put SENDGRID_API_KEY
SESSION_SECRET— any long random string; used to sign the session-id cookie. Required.SENDGRID_API_KEY— your SendGrid API key, used for the "forgot password" email. Optional — if omitted, the app logs the reset link to the console instead of emailing it (handy for local testing).
For local development, wrangler doesn't read wrangler secret put values —
create a .dev.vars file instead (already gitignored):
SESSION_SECRET=some-long-random-local-only-string
SENDGRID_API_KEY=
npm run db:migrate:local # local dev database (used by `wrangler dev`)
npm run db:migrate:remote # real Cloudflare D1 database (used by `wrangler deploy`)
npm run db:seed:local
npm run db:seed:remote
seed.sql is a plain-SQL port of the original seeds/*.js fixtures. All
seeded users share the password password123 (bcrypt hash baked into the
file — regenerate it with
node -e "require('bcryptjs').hash('password123', 10).then(console.log)"
if you want a different one).
npm run dev
This starts wrangler dev against the local D1/R2 simulators (no
Cloudflare account calls). Visit http://localhost:8787.
npm run deploy
Runs wrangler deploy, which builds against your remote D1/R2 resources.
Make sure you've run the --remote migrate/seed commands above at least once
first.
What changed from the original Heroku/Express app, and why:
- Express → Hono. Express's
app.listen()server model can't run on the Workers runtime (no Node HTTP server, no persistent process), so the router had to change. Hono is a thin, Workers-native router with an API similar enough to Express thatcontrollers/*.jsports over almost line-for-line intosrc/routes/*.js. - Views stayed Handlebars. The original
views/*.handlebarsfiles,views/partials/*.handlebars, andutils/helpers.js(renamedutils/helpers.cjs— see below) are unchanged. They're rendered with the realhandlebarsnpm package instead of the Express-onlyexpress-handlebarsmiddleware.- Why a precompile step: Cloudflare Workers blocks dynamic code
generation (
eval/new Function) for security, andHandlebars.compile()relies on exactly that. Soscripts/build-templates.mjsprecompiles every.handlebarsfile into plain JS (src/generated/templates.js, gitignored) ahead of time, and the Worker renders them with the eval-freehandlebars/runtimebuild. This runs automatically beforenpm run devandnpm run deploy(see thepredev/predeployscripts) — if you edit a.handlebarsfile, just re-runnpm run dev/deploy, ornpm run build:templatesmanually. utils/helpers.jswas renamed toutils/helpers.cjs(content byte-for-byte identical). This is required becausepackage.jsonnow sets"type": "module"for the Worker's own ESM code, and Node/esbuild only treat.cjsfiles as CommonJS unconditionally regardless of that setting — the file still uses the originalmodule.exports/require('crypto').utils/auth.js(the oldwithAuthExpress middleware) is kept for reference but isn't imported anywhere — Express's(req, res, next)signature doesn't exist in Hono. The equivalent guard isrequireAuth()insrc/lib/session.js, same behavior (redirects to/loginif there's no session).
- Why a precompile step: Cloudflare Workers blocks dynamic code
generation (
- Sequelize/MySQL → Drizzle/D1.
models/*.jsbecamesrc/db/schema.js, keeping the same table names, columns, and relationships (including the samesnake_casefield names Sequelize used, e.g.post_text,created_at,genre_id), so the Handlebars templates didn't need to change at all. Migrations live inmigrations/(generated withdrizzle-kit generate);seed.sqlreplacesseeds/*.js.- Fix: the original
comment/voteforeign keys toposthad no cascade behavior, so deleting a post with any comments or votes always failed with a foreign-key error (same bug existed under MySQL). Both now cascade-delete on post deletion. - Fix:
seeds/vote-seeds.jsinserted{user_id, genre_id}pairs, but theVotemodel requires{user_id, post_id}(post_idNOT NULL) — that seed was already broken upstream.seed.sqluses sensible{user_id, post_id}pairs instead.
- Fix: the original
- express-session + connect-session-sequelize → signed cookie + D1
sessiontable. The signed HTTP-only cookie holds only an opaque session id (via Hono'shono/cookiehelpers); the row in D1 holdsuser_id/username/emailand an expiry. This was chosen over a cookie that carries the session payload directly because it makes/api/users/logoutan actual server-side revocation (delete the row) — a payload-only signed cookie can't be invalidated before it expires. - bcrypt → bcryptjs.
bcryptis a native (C++) addon; Workers only run pure JavaScript/WASM.bcryptjsis a drop-in, pure-JS reimplementation, so existing password hashes and the hashing calls didn't need to change otherwise. - express-fileupload → R2. Uploaded images go to the
IMAGES_BUCKETR2 bucket (wrangler r2 bucket create killer-reads-images) instead ofpublic/assets/uploads/; aGET /uploads/:keyroute insrc/index.jsproxies objects back out of R2. Theimagetable keeps the same metadata columns, swapping the olddataBLOB column for anr2_key. - SendGrid is unchanged apart from calling the HTTP API directly via
fetch()(src/lib/email.js) instead of the@sendgrid/mailSDK, and reading the key fromenv.SENDGRID_API_KEY(a Workers secret) instead ofprocess.env. - Heroku artifacts removed: the
JAWSDB_URL/dotenv-basedconfig/connection.jsis gone (D1 is bound directly viawrangler.toml); there was noProcfileor.envin the repo to begin with. - Small security fixes made along the way (all pre-existing bugs in the
original app, not intentional behavior changes):
PUT /api/users/:idandDELETE /api/users/:idhad no auth check at all in the original app; they now require a session and only allow a user to modify their own account, matching whatPUT /api/posts/:idalready did.DELETE /api/posts/:iddidn't check ownership (any logged-in user could delete anyone's post); it now matches the ownership checkPUTalready had.- Signup/login responses no longer include the bcrypt password hash in the JSON body.
- Dropped as dead/broken code, not ported:
controllers/auth-routes.js(an unreachable duplicate ofhome-routes.js's login/signup pages —home-routeswas always registered first) and the standaloneGET /edit-postroute (redirected to itself when logged in — an infinite redirect bug; the real edit flow is/user-profile/edit-post/:id).


