A mobile-first LMS where every module is markdown (.mdx), authored with a WYSIWYG
(BlockNote), linked via [[wikilinks]] into an Obsidian-style graph, with
completion, scroll-%, reading time, and highlight/note tracking per learner.
- Next.js 16 App Router + Cache Components (PPR)
- Clerk auth with roles (
creator/learner, set viapublicMetadata.role) - Neon Postgres + Drizzle ORM
- Vercel Blob (public for
.mdx, private for PDFs) - Mistral OCR for PDF → markdown
- BlockNote WYSIWYG editor (serializes to markdown)
- next-mdx-remote/rsc + remark-gfm to render MDX
- shadcn/ui + Tailwind v4, mobile-first
-
cp .env.example .env.localand fill in values. -
Install via Vercel Marketplace (recommended):
- Neon Postgres (provides
DATABASE_URL) - Vercel Blob (provides
BLOB_READ_WRITE_TOKEN) - Clerk (provides Clerk keys)
- Neon Postgres (provides
-
Run the migration:
pnpm dlx dotenv-cli -e .env.local -- pnpm drizzle-kit push
-
Set one user's Clerk
publicMetadata.roleto"creator"(Clerk dashboard → Users). -
Add
MISTRAL_API_KEY(or route via AI Gateway withAI_GATEWAY_API_KEY). -
pnpm dev→ http://localhost:3000
/— Library/m/[slug]— Reader (progress tracking, mark-complete, highlights)/graph— Force-directed graph of modules & wikilinks/records— Completions, time spent, notes/studio— Creator-only module list/studio/new— Create module/studio/[id]— Edit module/studio/import— PDF → OCR → editor
proxy.ts— Clerk middleware + role gatesrc/db/schema.ts— Drizzle schemasrc/lib/mdx/wikilinks.ts—[[link]]extractor + transformersrc/lib/mdx/render.tsx— MDX renderersrc/lib/ocr/mistral.ts— PDF → markdownsrc/app/api/modules/route.ts— Save module (Blob write, wikilink parse, cache invalidate)src/app/api/progress/route.ts— sendBeacon target for scroll/time/completionsrc/app/api/notes/route.ts— Highlights + notessrc/components/editor/blocknote-editor.tsx— WYSIWYGsrc/components/reader/reader-client.tsx— Scroll tracker, FAB, highlight toolbar
MDX exports/diffs like plain .md but enables embedded components (callouts, quizzes, video) without sidecar JSON. Avoid MDX-only syntax in modules you want to edit in Obsidian itself.