A personal note-taking web app with a WYSIWYG rich text editor, full-text search, and a clean responsive UI. Built as a single-user app, accessible as a PWA on mobile.
| Layer | Technology |
|---|---|
| Frontend | React 19, Vite, Tiptap (ProseMirror), Tailwind CSS 4, React Query |
| Backend | Node.js, Express, TypeScript |
| Database | PostgreSQL (Prisma ORM) |
| Search | PostgreSQL full-text search (tsvector + GIN index) |
| Auth | Single password with bcrypt + JWT |
| Deployment | Docker on Railway |
- WYSIWYG rich text editor — Tiptap-based editor where bold text looks bold, headings look like headings, tables are visual grids, and task lists have checkboxes — no Markdown syntax required
- Block-level editing — Large documents (50KB+) render as fast static HTML; clicking any block activates just that block in a live editor, preventing browser freezes on very large notes
- Formatting toolbar — Toggleable toolbar for bold, italic, underline, strikethrough, headings, code, links, lists, blockquotes, tables, and horizontal rules; buttons highlight to show active formats; available in both the note editor and scratchpad
- Table of contents — Auto-generated panel listing all headings with click-to-jump navigation; toggle via the tree icon in the action bar
- Table editing — Right-click inside a table for a context menu to add/delete rows and columns, toggle header rows, merge/split cells, or delete the table
- Fullscreen mode — Expand the editor to fill the entire page; exit with Escape
- Global search — Weighted PostgreSQL FTS across all notes (title boosted over content), with highlighted result snippets
- Notebooks — Organize notes in a hierarchical tree with right-click context menu for moving, renaming, and creating sub-notebooks
- Favourites — Pin folders or notes to a dedicated section at the top of the sidebar for quick access
- Drag-and-drop — Move notes and folders in and out of other folders directly in the sidebar tree
- Root notes — Create notes outside of any notebook; they appear in the sidebar below the notebook tree and open directly in the editor
- Scratchpad — Instant-access editor on app load for quick jotting; auto-saved and always available
- Auto-save — 1-second debounce, saves in the background
- Responsive layout — 3-column desktop, 2-column tablet, single-column mobile with bottom navigation
- 5 themes — Light, Dark, Rose, Lavender, and Mint; preference saved across sessions
- PWA — Installable on Android via "Add to Home Screen"
beijer.ink/
├── client/ # React + Vite frontend
│ ├── src/
│ │ ├── api/ # Axios API wrappers
│ │ ├── components/ # UI components (layout, editor, notes, search, auth)
│ │ ├── editor/ # Tiptap extensions (search highlighting)
│ │ ├── hooks/ # React hooks (useAuth, useAutoSave, useTiptap, useSearch)
│ │ └── types/ # TypeScript type definitions
│ └── public/ # PWA manifest, icons, service worker
├── server/ # Express API backend
│ ├── src/
│ │ ├── routes/ # Express route definitions
│ │ ├── controllers/ # Request handlers
│ │ ├── services/ # Business logic + database queries
│ │ ├── middleware/ # Auth, validation, error handling
│ │ ├── validators/ # Zod schemas
│ │ └── lib/ # Prisma client, R2 client, utilities
│ └── prisma/ # Schema + migrations
├── scripts/ # Seed password, Google Drive auth setup
└── Dockerfile # Multi-stage production build
- Node.js 22+
- PostgreSQL database (local or hosted)
-
Clone the repo and install dependencies:
git clone https://github.com/michaelbeijer/beijer.ink.git cd beijer.ink npm install -
Create a
.envfile from the example:cp .env.example .env
Fill in your
DATABASE_URL,JWT_SECRET(64-char hex string), andADMIN_PASSWORD. -
Copy
.envto the server directory (Prisma requires it there):cp .env server/.env
-
Run database migrations:
npm run db:migrate
-
Seed the admin password:
npm run seed
-
Start the development server:
npm run dev
The client runs on
http://localhost:5173and the API onhttp://localhost:3000.
Build and start in production mode:
npm run build
npm startOr use Docker:
docker build -f server/Dockerfile -t beijer-ink .
docker run -p 3000:3000 --env-file .env beijer-inkAll endpoints under /api, JWT-protected except login.
| Method | Path | Description |
|---|---|---|
POST |
/api/auth/login |
Authenticate with password |
GET |
/api/auth/verify |
Verify JWT token |
GET |
/api/notebooks |
List all notebooks |
POST |
/api/notebooks |
Create notebook |
PATCH |
/api/notebooks/:id |
Update notebook |
DELETE |
/api/notebooks/:id |
Delete notebook |
GET |
/api/notes/root |
List root-level notes (no notebook) |
GET |
/api/notes/notebook/:notebookId |
List notes in notebook |
GET |
/api/notes/:id |
Get single note |
POST |
/api/notes |
Create note |
PATCH |
/api/notes/:id |
Update note (auto-save) |
DELETE |
/api/notes/:id |
Delete note |
PATCH |
/api/notes/:id/move |
Move note to another notebook |
GET |
/api/scratchpad |
Get scratchpad content |
PUT |
/api/scratchpad |
Update scratchpad content |
GET |
/api/search?q=... |
Full-text search with highlighted snippets |
GET |
/api/backup/download |
Download all notes as a zip of markdown files |
POST |
/api/backup/google-drive/run |
Upload backup to Google Drive |
- Create a new Railway project with a PostgreSQL add-on
- Add a web service pointing to this repo
- Set environment variables:
DATABASE_URL,JWT_SECRET,ADMIN_PASSWORD,NODE_ENV=production - Railway will build using the Dockerfile and run migrations on startup
- Configure your custom domain in Railway settings
Automatic daily backups upload a ZIP of all notes (as markdown files preserving notebook folder structure) to your Google Drive. You can also trigger an upload manually from Settings with "Run Google Drive Backup Now".
- Create an OAuth client (Desktop app) in Google Cloud Console with the Drive API enabled
- Run the one-time auth script to get a refresh token:
npx tsx scripts/google-drive-auth.ts
- Set these environment variables on Railway:
BACKUP_ENABLED=trueBACKUP_CRON=0 2 * * *(default: 2 AM daily)BACKUP_TIMEZONE=Europe/LondonGOOGLE_DRIVE_CLIENT_ID=...GOOGLE_DRIVE_CLIENT_SECRET=...GOOGLE_DRIVE_REFRESH_TOKEN=...GOOGLE_DRIVE_FOLDER_ID=...(optional — omit to upload to Drive root)
This project is for personal use. All rights reserved.