MultiNotes is a minimalist, full-screen block-based notes workspace for Cloudflare Workers. The frontend is a React single-page app built with Vite; each note pane is an Editor.js block editor with fluid Notion-style block dragging. Persistence is per-user via Hono, Drizzle ORM, and Cloudflare D1.
- Multiple block editors side-by-side in one full-desktop workspace
- Editor.js blocks: headings, lists, checklists, quotes, images, and paragraphs
- Per-block text alignment (left/center/right) from the block settings menu
- Drag blocks within a note or across notes, including a multi-block selection
- Drag-and-drop reordering of note panes (dnd-kit), persisted to D1
- Slash menu (
/) and inline toolbar for block creation and formatting - Inline images stored as base64 in the note content
- Links hint to hold Cmd/Ctrl to open them in a new tab
- Read-only public share links (
/p/<token>) toggled from settings - Debounced auto-save to D1 as Editor.js JSON
- Legacy HTML notes are converted to Editor.js blocks automatically on first open
- Email/password auth with JWT cookies
- Optional Cloudflare Access mode for private deployments
- Light and dark themes
- Cloudflare Workers + Hono (API)
- React 18 + Vite +
@cloudflare/vite-plugin(single-page app) - Editor.js (block editor) with
editorjs-drag-drop - dnd-kit (note pane reordering)
- Cloudflare D1 SQLite + Drizzle ORM and Drizzle Kit
- Vitest with
@cloudflare/vitest-pool-workers(worker) and happy-dom (client)
- Node.js 22 or newer
- npm
- A Cloudflare account for D1 and deployment
- Wrangler authentication for deployment:
npx wrangler login
# Install dependencies
npm install
# Create local env vars
cp .dev.vars.example .dev.varsEdit .dev.vars and set:
JWT_SECRET=replace-with-a-long-random-string
ENABLE_AUTH=trueApply the D1 migration locally:
npx wrangler d1 migrations apply multinotes --localStart the Vite dev server (serves the React app and runs the Worker via @cloudflare/vite-plugin):
npm run devOpen the URL Vite prints (e.g. http://localhost:5173).
With ENABLE_AUTH=true, register an account on the login screen, then the workspace loads automatically. Note: for local development, set ENABLE_AUTH in wrangler.toml (the Vite plugin reads [vars] from it); Cloudflare Access mode requires the access header and is intended for deployed environments.
Set:
[vars]
ENABLE_AUTH = "true"Users register and sign in with email/password. Passwords are hashed with PBKDF2 using the Web Crypto API. Sessions use an httpOnly JWT cookie signed by JWT_SECRET.
Set:
[vars]
ENABLE_AUTH = "false"In this mode, MultiNotes expects Cloudflare Access to provide the Cf-Access-Authenticated-User-Email header. Users are created automatically by email and no password is stored.
Create a D1 database:
npx wrangler d1 create multinotesCopy the returned database_id into wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "multinotes"
database_id = "your-d1-database-id"
migrations_dir = "src/db/migrations"Set a JWT secret:
npx wrangler secret put JWT_SECRETApply migrations remotely:
npx wrangler d1 migrations apply multinotes --remoteDeploy:
npm run deployThe deploy script runs typecheck, tests, lint, the Vite build, remote D1 migrations, and wrangler deploy.
npm run dev # Start the Vite dev server (React app + Worker)
npm run build # Build the SPA and Worker into dist/
npm test # Run Vitest (worker + client projects)
npm run lint # Run ESLint
npm run typecheck # Type-check the Worker and the client
npm run deploy # Validate, build, migrate remote D1, and deployindex.html Vite entry for the React single-page app
src/
index.ts Hono API entry (non-API routes served as SPA assets)
middleware/auth.ts JWT cookie or Cloudflare Access user resolution
routes/auth.ts Register, login, logout, and current-user routes
routes/workspaces.ts Workspace CRUD, share links, and ordered note loading
routes/notes.ts Note create, update, delete, and reorder routes
db/schema.ts Drizzle schema for users, workspaces, and notes
db/index.ts Drizzle D1 client factory
lib/password.ts PBKDF2 password hashing and verification
lib/workspaces.ts Main workspace creation helper
client/ React single-page app
main.tsx React entry
App.tsx Auth bootstrap (login vs. workspace)
api.ts Typed fetch client for /api
components/ Workspace, NotePane, PublicView/PublicNote, settings, login
editor/ Editor.js tools, alignment tune, cross-note drag, parsing
hooks/ useTheme, useDebouncedSave, useLinkTooltip
styles/global.css App and Editor.js theming
test/
*.test.ts Worker, API, auth, and routing tests
legacy/ Previous vanilla-JS editor, kept for reference
GET /api/config # { authEnabled } — SPA reads this to gate login
POST /api/auth/register
POST /api/auth/login
POST /api/auth/logout
GET /api/auth/me
GET /api/workspaces
POST /api/workspaces
GET /api/workspaces/:id
PATCH /api/workspaces/:id
GET /api/workspaces/:id/share
PATCH /api/workspaces/:id/share
DELETE /api/workspaces/:id/share
POST /api/workspaces/:wid/notes
PATCH /api/workspaces/:wid/notes/:nid
DELETE /api/workspaces/:wid/notes/:nid
PUT /api/workspaces/:wid/notes/order
- The app targets modern desktop browsers (Chrome, Edge, Firefox, Safari).
- Note content is stored as Editor.js
OutputDataJSON in thenotes.htmlcolumn. Legacy notes stored as raw HTML are converted to blocks in the browser on first open and re-saved as JSON (one-time, per note). - Images are stored inline as base64 in the note content. This is convenient, but large images increase D1 row size.
- Non-API routes are served as the React SPA via the assets binding (SPA fallback); auth gating happens client-side while the API stays protected server-side.
- The previous vanilla-JS
contenteditableeditor is preserved underlegacy/purely for reference. Its text alignment, public share view, multi-select cross-note drag, and link tooltips have since been reimplemented natively in the React/Editor.js app; per-character font sizes were intentionally dropped.