My Study Notes
A self-hostable, Notion-style notes app for publishing a personal knowledge base. Write rich documents in a block editor, organize them into a nested tree, and publish individual notes as clean, SEO-friendly public pages — while keeping editing locked to a single owner account.
Built on Next.js (App Router), Convex, Clerk, BlockNote, and Cloudflare R2.
Live demo: studynotes.terrydjony.com
Features
- 📝 Block editor — rich text powered by BlockNote (headings, lists, checkboxes, code blocks, images, and more).
- ⚡ Slash commands — type
/for blocks, plus a custom page link command to link to any of your other notes inline. - 🌲 Nested hierarchy — organize notes into an arbitrarily deep tree of pages.
↕️ Drag-and-drop reordering — reorder pages in the sidebar (owner-only), built with@dnd-kit.- 🖼️ Image uploads — cover images and inline images stored in Cloudflare R2 via presigned uploads.
- 🙂 Icons & covers — pick an emoji icon and a cover image per note.
- 🔎 Quick search — a
⌘Kcommand palette to jump between notes. - 🌗 Light / dark / system theme — instant theme switcher in the sidebar (next-themes).
↔️ Adjustable sidebar — resizable and collapsible, mobile-friendly.- 🗑️ Trash — archive, restore, and permanently delete notes.
- 🌍 Publish & SEO — publish a note to a public
/notes/<slug>page with auto-generated Open Graph / Twitter cards (Satori), JSON-LD structured data, canonical tags,sitemap.xml, androbots.txt. - 🔐 Single-owner model — anyone can read published notes, but only the
configured
OWNER_EMAILaccount can create or edit. Authentication via Clerk (Google OAuth); the owner email is compared server-side in Convex and never exposed to the client. - 🔄 Real-time — every change syncs instantly via Convex.
Tech stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, RSC), React 19 |
| Language | TypeScript |
| Backend / DB | Convex (real-time database + serverless functions) |
| Auth | Clerk (Google OAuth) |
| Editor | BlockNote |
| Styling | Tailwind CSS + shadcn/ui + Radix UI |
| Icons | lucide-react |
| File storage | Cloudflare R2 (S3-compatible, presigned uploads) |
| State | Zustand, usehooks-ts |
| Drag & drop | @dnd-kit |
| Misc | sonner (toasts), zod, emoji-picker-react, react-dropzone |
Getting started
Prerequisites
- Node.js 18+ and npm
- A Convex account
- A Clerk account
- A Cloudflare R2 bucket (for image uploads)
1. Clone & install
git clone <your-repo-url> cd my-study-notes npm install
2. Set up Convex
This provisions a dev deployment and writes CONVEX_DEPLOYMENT and
NEXT_PUBLIC_CONVEX_URL into your .env.local. Leave it running in a terminal
while developing.
3. Set up Clerk
- Create an application in the Clerk dashboard and enable Google as a social connection.
- Copy your Publishable key and Secret key into
.env.local(NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,CLERK_SECRET_KEY). - Create a JWT template named exactly
convex. Copy its Issuer URL. - In the Convex dashboard → Settings → Environment Variables, set
CLERK_JWT_ISSUER_DOMAINto that Issuer URL. (This is read byconvex/auth.config.js; do not put it in.env.local.)
4. Set up Cloudflare R2
- Create an R2 bucket.
- Create an R2 API token (Object Read & Write) and copy the Account ID, Access Key ID, and Secret Access Key.
- Enable the bucket's Public Development URL (
pub-xxxx.r2.dev) or attach a custom domain, and use it asNEXT_PUBLIC_R2_PUBLIC_URL. - Fill in
R2_ACCOUNT_ID,R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY,R2_BUCKET_NAME, andNEXT_PUBLIC_R2_PUBLIC_URLin.env.local.
5. Configure the site owner
Editing is restricted to one account. Set OWNER_EMAIL to the Google email you
sign in with — in both places:
# For the Next.js upload route (.env.local) OWNER_EMAIL=you@example.com # For Convex write mutations (deployment env) npx convex env set OWNER_EMAIL you@example.com
Anyone can read published notes; only this account can create/edit.
6. Run
Open http://localhost:3000. Sign in with the owner account to start writing.
Environment variables
See .env.example for the full annotated list. Summary:
| Variable | Where | Purpose |
|---|---|---|
CONVEX_DEPLOYMENT |
.env.local |
Set by npx convex dev |
NEXT_PUBLIC_CONVEX_URL |
.env.local |
Set by npx convex dev |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
.env.local |
Clerk publishable key |
CLERK_SECRET_KEY |
.env.local |
Clerk secret key |
CLERK_JWT_ISSUER_DOMAIN |
Convex env | Issuer URL of the convex JWT template |
R2_ACCOUNT_ID |
.env.local |
Cloudflare R2 account ID |
R2_ACCESS_KEY_ID |
.env.local |
R2 API token access key |
R2_SECRET_ACCESS_KEY |
.env.local |
R2 API token secret |
R2_BUCKET_NAME |
.env.local |
R2 bucket name |
NEXT_PUBLIC_R2_PUBLIC_URL |
.env.local |
Public base URL the bucket is served from |
NEXT_PUBLIC_SITE_URL |
.env.local |
Canonical base for sitemap / OG / canonical tags |
OWNER_EMAIL |
both | The single account allowed to edit |
Project structure
app/ Next.js App Router
(main)/ Authenticated editor app (sidebar, documents)
notes/[slug]/ Public published note pages (+ OG/Twitter images)
api/upload/ Presigned R2 upload/delete route (owner-gated)
sitemap.ts robots.ts SEO endpoints
components/ Editor, toolbar, modals, shadcn/ui primitives
convex/ Schema, document queries/mutations, auth config
hooks/ Client hooks (owner check, search, cover image, ...)
lib/ R2 client, slugify, note rendering, upload helpers
Deployment
Deploy the frontend to Vercel and push your Convex
functions to a production deployment with npx convex deploy. Use Clerk
production keys and set every environment variable above (including
CLERK_JWT_ISSUER_DOMAIN and OWNER_EMAIL in the production Convex deployment,
and NEXT_PUBLIC_SITE_URL to your real domain).
Credit
Built by terrydjony.com. Originally inspired by AntonioErdeljac/notion-clone-tutorial.
License
MIT
