Opinionated, authenticated Markdown knowledge base with a rich ProseMirror editor, automatic saving, S3-backed asset uploads, per‑user quota & size enforcement, and shareable read‑only viewer pages – all built on Next.js 16 (App Router + RSC), React 19, tRPC 11, Prisma 7, Clerk, shadcn/ui & TailwindCSS 4.
- Authenticated workspace (Clerk) – all
/api+/documentsroutes protected by edge middleware. - Rich Markdown authoring powered by a ProseMirror-based editor with:
- Inline formatting: bold, italic, underline, strikethrough, subscript, superscript, inline code
- Block types: headings (H1-H6), paragraphs, blockquotes, horizontal rules
- Lists: bullet lists, ordered lists, task lists with checkboxes
- Code blocks with syntax highlighting (via refractor) and language selector
- Math equations via KaTeX (inline and block)
- Mermaid diagram support with live preview, SVG/PNG export, and expanded full-page overlay
- Link editing with inline bubble menu for quick URL edits
- Image uploads with alignment controls, drag-to-resize, and captions
- Block-level editing with drag handle for reordering and contextual menus for block actions (turn into, duplicate, delete)
- Clipboard intelligence: Copy/paste as Markdown, automatic Markdown detection on paste, styled HTML export
- Autosave (1s debounce + background beacon on unload / navigation) with optimistic sidebar ordering
- Responsive design with mobile-friendly bottom toolbar positioning
- Import existing
.mdfiles – server derives title from front‑matter or first#heading - Export raw Markdown (
/api/download/:docId) - Embedded asset management: direct S3 presigned POST uploads; unused assets soft‑deleted on content update
- Per‑user file quota (100MB) & document size enforcement (configurable)
- Shareable read‑only viewer pages (
/viewer/:docId) with SSR prefetch + hydration via TanStack Query - File garbage collection API endpoint (
/api/cleanup) for cleaning up soft-deleted files with configurable grace period - Fully typed end‑to‑end API via tRPC (no separate REST client types needed)
Document(id, userId, title, content, createdAt, updatedAt)
File(id, userId, fileKey, fileName, fileType, fileSize?, documentId?, deletedAt?, createdAt, updatedAt)
Relations: Document 1 - n File. Files are soft‑deleted (set deletedAt) when no longer referenced in the document body (/api/file/<fileKey> pattern).
| Layer | Purpose |
|---|---|
| Next.js App Router (RSC) | Layout composition, server data prefetch (Query Client + tRPC). |
| tRPC 11 | Type‑safe procedures (document.*). |
| Prisma 7 | Postgres ORM with @prisma/adapter-pg; generated client in lib/generated/prisma. |
| Clerk | Auth context injection in tRPC + middleware route protection. |
| S3-Compatible Storage | Binary asset storage; presigned POST for direct browser uploads. |
| Zustand Store | Local editor state + saved vs dirty diffing with version-based document switching. |
| TanStack Query | Client cache hydration + reactivity for procedures. |
| ProseMirror | Rich text editing with custom schema, node views, and plugins. |
| Mermaid (iframe-sandboxed) | Diagram rendering via isolated iframe for security. |
- User edits title/content (Zustand current state updates
isSaved=false). - Debounced effect (1s) triggers
document.setDocumentmutation. - On success: saved state updated; documents list re‑ordered with updated doc first (manual cache manipulation).
- On unload / route change: a
navigator.sendBeaconensures last edits persist even if the debounce window not reached.
- User requests upload: client calls
/api/uploadwith file metadata. - Server checks remaining quota, creates DB
File(size null) + returns presigned POST (URL + fields + key). - Browser uploads directly to S3.
- Client invokes
confirmUploadserver action withfileKey+documentId– server queries S3 HEAD to recordfileSizeand associates file to document. - On document save, server parses markdown for
/api/file/<key>references; unreferenced files are soft‑deleted. - Cleanup endpoint (
/api/cleanup) permanently removes files past grace period from both S3 and database.
- Node.js 20+
- pnpm 10.14+ (see
packageManagerfield) - Postgres database
- S3-compatible storage bucket & credentials
- Clerk application (publishable + secret keys)
pnpm installCreate .env.local (or supply via deployment platform). Suggested template:
DATABASE_URL=postgresql://user:pass@host:5432/db
DOCUMENT_MAX_BYTES=200000
NEXT_PUBLIC_DOCUMENT_MAX_BYTES=200000
AWS_REGION=us-east-1
AWS_S3_BUCKET_NAME=your-bucket
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=...
CLERK_SECRET_KEY=...
CLEANUP_API_KEY=your-secret-key-for-cleanup
Notes:
DOCUMENT_MAX_BYTES(server) andNEXT_PUBLIC_DOCUMENT_MAX_BYTES(client) should match; client performs early pre‑check, server is authoritative.- Changing size limits requires a dev server restart to update the server constant.
CLEANUP_API_KEYis required for the garbage collection endpoint.
Generate & apply migrations (already present migrations will apply):
pnpm prisma migrate devGenerate Prisma Client (runs automatically on migrate):
pnpm prisma generatepnpm devVisit http://localhost:3000 – unauthenticated users will be redirected to sign‑in for protected routes.
pnpm build && pnpm start| Procedure | Description |
|---|---|
document.importMarkdown(content, title?) |
Create new document from raw markdown (derives title if absent). Size enforced. |
document.getById(id) |
Fetch single document. Public procedure but effective access depends on middleware. |
document.getAllDocumentsForUser() |
List current user documents (desc by updatedAt). |
document.newDocument() |
Create empty document with default title. |
document.setDocument({id, title?, content?}) |
Partial update + file reference sync. |
document.renameDocument({id, title}) |
Title-only update. |
document.deleteDocument({id}) |
Hard delete document + soft-delete associated files. |
| Endpoint | Method | Description |
|---|---|---|
/api/upload |
POST | Returns S3 presigned POST + creates File record (size null). Enforces remaining quota. |
/api/file/:key |
GET | Redirects to public S3 object URL (simple indirection layer). |
/api/download/:docId |
GET | Raw markdown download with Content-Disposition attachment. |
/api/cleanup |
POST | Garbage collection - permanently deletes soft-deleted files past grace period. Requires API key. |
/api/cleanup |
GET | Status check - returns counts of pending, in-grace-period, and orphaned files. |
confirmUpload({fileKey, documentId}) – After direct upload, records file size via S3 HeadObject and associates file.
- Clerk middleware guards
/api&/documentsroutes;/viewerroutes are public for read access. - Procedure-level auth:
protectedProcedureenforcesuserIdpresence; public read limited todocument.getByIdinside router but effective exposure depends on middleware.
- Document: UTF‑8 byte length measured with
TextEncoder; error if >DOCUMENT_MAX_BYTES(default: 200KB). - Files: Quota is 100MB per user (simple sum of recorded
fileSize). Upload route sets dynamic max object size in presigned POST conditions.
Soft-deleted files are cleaned up via the /api/cleanup endpoint:
# Run cleanup (requires API key)
curl -X POST "https://your-domain.com/api/cleanup" \
-H "Authorization: Bearer YOUR_CLEANUP_API_KEY"
# Check cleanup status
curl "https://your-domain.com/api/cleanup" \
-H "Authorization: Bearer YOUR_CLEANUP_API_KEY"Options:
?gracePeriodHours=24– Override default 24-hour grace period- Maximum 1000 files processed per run
Cleanup handles:
- Files soft-deleted past the grace period
- Orphaned uploads (files where upload was never confirmed)
Typical (e.g. Vercel):
- Provision Postgres & run migrations (
prisma migrate deploy). - Set environment variables (including both Clerk keys, size configs, and cleanup API key).
- Build with
pnpm build. - Start with
pnpm start(production). Edge middleware runs at the platform edge. - Set up a cron job to call
/api/cleanupperiodically (e.g., daily).
Ensure your S3 bucket CORS allows presigned POST from your origin & GET for public viewing.
app/
documents/ # Editor pages (protected)
viewer/ # Public read-only rendering
api/
cleanup/ # File garbage collection endpoint
download/ # Markdown export
file/ # S3 file proxy
upload/ # Presigned POST generation
trpc/ # tRPC API handler
components/
editor/
prosemirror/ # ProseMirror editor implementation
schema.ts # Custom schema (code blocks, images, equations, etc.)
markdown.ts # Markdown parser/serializer
plugins/ # Custom plugins (clipboard, drag-handle, block-handle)
*-node-view.tsx # Custom node views (code blocks, images, equations)
auto-save-observer.tsx
content-editor.tsx
title-editor.tsx
layout/ # Sidebar, header, responsive shell
viewer/ # Markdown display components
ui/ # shadcn/ui components + custom (mermaid-visualizer, svg-viewer)
lib/
trpc/ # tRPC setup (server + client bindings)
actions/ # Server actions (upload confirmation)
hooks/ # Zustand editor store, debounce, media query hooks
mermaid/ # Iframe-sandboxed Mermaid rendering system
prisma/ # Schema + migrations
MIT
Made with ❤️ using Next.js, tRPC, Prisma & ProseMirror.
