KnowledgeBase is a lightweight, self-hosted personal knowledge management application that solves the problem of fragmented note-taking by providing a unified, privacy-first platform for creating, organizing, searching, and sharing markdown
Master briefing file with problem statement, dev commands, and architecture principles.
KnowledgeBase is a lightweight, self-hosted personal knowledge management application that solves the problem of fragmented note-taking by providing a unified, privacy-first platform for creating, organizing, searching, and sharing markdown-based notes.
Unlike cloud-hosted solutions (Notion, Obsidian Sync, Evernote), this application runs entirely on the user's own infrastructure, ensuring full data ownership and control. It targets individuals and small teams who need structured knowledge management with tag-based organization, full-text fuzzy search, and read-only sharing—without the complexity or cost of enterprise tools.
Core problems addressed:
#project/2024/roadmap) provide structured categorization beyond flat folders..md, JSON bundles) ensures data portability.What it is NOT:
| User Segment | Description | Primary Use Case |
|---|---|---|
| Individual Knowledge Workers | Solo users managing personal wikis, research notes, project documentation | Daily note capture, structured organization, quick retrieval |
| Small Teams (2–10) | Co-located or distributed teams needing a lightweight internal wiki | Shared knowledge base with read-only public links |
| Developers & Engineers | Technical users who prefer markdown and want code-friendly note management | Architecture docs, runbooks, meeting notes with code snippets |
| Researchers & Academics | Users managing literature notes, reference collections, and idea trails | Cross-referencing notes via tags, exporting for papers |
| KPI | Target | Measurement Method |
|---|---|---|
| Note creation throughput | < 500ms from click to saved note visible | API response time (p95) for POST /api/v1/notes |
| Search latency | < 1s for queries across up to 10,000 notes | Full-text search endpoint response time |
| Authentication security | Zero sessions compromised via XSS/CSRF | HttpOnly cookie enforcement, JWT expiry ≤ 24h, bcrypt hashing |
| Export/import reliability | 100% fidelity round-trip (export → import) | Integration test: export JSON → import → compare note count + content hash |
| Time to first note | < 3 minutes from container start | Measured from docker-compose up to successful note creation via UI |
| Uptime (self-hosted) | 99.9% achievable with Docker restart policy | Health check endpoint /api/v1/health |
┌─────────────────────────────────────────────────────────┐
│ Client (Browser) │
│ Next.js 16 (App Router) + Tailwind CSS v4 │
│ TypeScript · React 19 │
└──────────────────────┬──────────────────────────────────┘
│ HTTP (JSON/Markdown)
┌──────────────────────▼──────────────────────────────────┐
│ API Gateway / Express 5 │
│ Node.js 20 · RESTful /api/v1/* │
│ JWT Auth Middleware · Validation · Error Handling │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ PostgreSQL 16 │
│ Drizzle ORM (type-safe) │
│ Full-text search via pg_trgm extension │
└─────────────────────────────────────────────────────────┘
| Decision | Choice | Rationale | Trade-off |
|---|---|---|---|
| Frontend Framework | Next.js 16 (App Router) | SSR/SSG support, built-in routing, API route handlers reduce micro-service sprawl, excellent DX with TypeScript | Slightly heavier than plain Vite+React; increases build complexity |
| Styling | Tailwind CSS v4 | Utility-first, zero-runtime CSS; v4 offers CSS-native configuration, faster builds, and tighter integration with Next.js | Learning curve for utility-class sprawl; less semantic than CSS modules for complex components |
| Backend | Express 5 | Minimal, unopinionated; vast middleware ecosystem; Express 5 brings modern async error handling and performance improvements | Requires manual wiring of routes/middleware vs. NestJS's batteries-included approach |
| Database | PostgreSQL | Robust full-text search (tsvector + pg_trgm for fuzzy matching), JSONB for flexible metadata, ACID compliance, mature ecosystem | Heavier than SQLite for single-user deployment; requires separate service in Docker-compose |
| ORM | Drizzle | Type-safe, SQL-like syntax, zero overhead, excellent migration support, no code generation step | Smaller community than Prisma; fewer abstractions means more raw SQL exposure |
| Auth | JWT (access + refresh tokens) in httpOnly cookies | Stateless, scalable, no server-side session store needed; httpOnly cookies mitigate XSS token theft | Token revocation requires a denylist table; more complex logout than session-based auth |
| Password Hashing | bcrypt (cost factor 12) | Battle-tested, adaptive hashing, built-in salt management | Slightly slower than Argon2id but universally supported |
| Markdown Rendering | marked + highlight.js | Fast parsing, GitHub-Flavored Markdown support, syntax highlighting | No WYSIWYG editing; users must author in raw markdown |
| Fuzzy Search | PostgreSQL pg_trgm extension | Database-native, no external search service (Elasticsearch/Solr), supports similarity operators (%, <->) | Less fuzzy power than dedicated search engines; sufficient for <100K notes |
| Containerization | Docker Compose (v2) | Single-command deployment, reproducible environments, service dependency management | Not suitable for production scaling without Kubernetes or similar orchestration |
| Language | TypeScript (full-stack) | End-to-end type safety, shared validation schemas (Zod), reduced runtime errors | Adds build step; slightly slower compile times vs. JavaScript |
While SQLite simplifies single-node deployment (embedded, no separate service), PostgreSQL was selected because:
tsvector + pg_trgm outperforms SQLite's FTS5 for fuzzy matching.Trade-off accepted: Additional Docker service increases docker-compose up complexity. Mitigated by providing a docker-compose.yml with health checks and automatic restart.
Express 5 is used as a dedicated API server despite Next.js having built-in API routes because:
/api/v1/* prefixing without route group complexity.knowledgebase/
├── apps/
│ ├── web/ # Next.js 16 frontend
│ │ ├── src/
│ │ │ ├── app/ # App Router pages & layouts
│ │ │ ├── components/ # Shared UI components
│ │ │ ├── lib/ # Utilities, hooks, API clients
│ │ │ └── styles/ # Global styles, Tailwind config
│ │ ├── package.json
│ │ └── next.config.ts
│ └── api/ # Express 5 backend
│ ├── src/
│ │ ├── routes/ # RESTful route handlers (/api/v1/*)
│ │ ├── middleware/ # Auth, validation, error handling
│ │ ├── services/ # Business logic layer
│ │ ├── models/ # Drizzle ORM schema definitions
│ │ ├── db/ # Connection pool, migrations
│ │ └── index.ts # Express app entry point
│ └── package.json
├── packages/
│ ├── shared/ # Shared types, Zod schemas, constants
│ │ └── src/index.ts
│ └── cli/ # Seed & migration CLI tools
│ └── src/index.ts
├── docker-compose.yml
├── docker-compose.dev.yml
├── .env.example
├── pnpm-workspace.yaml
├── turbo.json
└── README.md
Root (workspace):
# Install all workspace dependencies
pnpm install
# Build all packages/apps
pnpm build
# Run all services in development mode
pnpm dev
Frontend (apps/web):
pnpm dev # Start Next.js dev server on :3000
pnpm build # Production build to .next/
pnpm start # Start production server (requires pnpm build first)
pnpm lint # ESLint check
Backend (apps/api):
pnpm dev # Start Express with ts-node-dev on :4000
pnpm build # Compile TypeScript to dist/
pnpm start # Run compiled production server
pnpm db:migrate # Run Drizzle migrations
pnpm db:push # Push schema to DB (dev only)
CLI (packages/cli):
pnpm seed # Seed database with sample data (5 users, 50 notes, tags)
pnpm seed:dev # Seed with minimal data (1 user, 5 notes)
# Full deployment (all services)
docker compose up --build
# Development with hot-reload
docker compose -f docker-compose.dev.yml up
# View logs
docker compose logs -f api web postgres
# Stop all services
docker compose down
# Reset database (dev only)
docker compose down -v && docker compose up
.env.example# ============================================
# KnowledgeBase — Environment Configuration
# ============================================
# --- Application ---
NODE_ENV=development
PORT=4000
API_BASE_URL=http://localhost:4000
WEB_BASE_URL=http://localhost:3000
# --- Database (PostgreSQL) ---
DATABASE_URL=postgresql://knowledgebase:secret@postgres:5432/knowledgebase?schema=public
# For local SQLite fallback (not recommended):
# DATABASE_URL=sqlite://./data.db
# --- JWT Authentication ---
# Generate with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
JWT_ACCESS_SECRET=your_64_char_minimum_access_secret_here_change_me_in_production
JWT_REFRESH_SECRET=your_64_char_minimum_refresh_secret_here_change_me_in_production
JWT_ACCESS_EXPIRY=15m # Access token lifetime (15 minutes)
JWT_REFRESH_EXPIRY=7d # Refresh token lifetime (7 days)
# --- Email Service (for password reset, optional) ---
# SMTP configuration for sending password reset emails.
# If not configured, password reset is disabled.
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_app_password_here
SMTP_FROM=KnowledgeBase <noreply@knowledgebase.local>
# --- OAuth (Optional — Google/GitHub) ---
GOOGLE_CLIENT_ID=your_google_oauth_client_id
GOOGLE_CLIENT_SECRET=your_google_oauth_secret
GITHUB_CLIENT_ID=your_github_oauth_client_id
GITHUB_CLIENT_SECRET=your_github_oauth_secret
CALLBACK_URL=http://localhost:4000/api/v1/auth/callback
# --- Rate Limiting ---
RATE_LIMIT_WINDOW_MS=900000 # 15-minute window
RATE_LIMIT_MAX_REQUESTS=100 # Max requests per window per IP
# --- CORS ---
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:4000
# --- File Uploads (for JSON import) ---
MAX_UPLOAD_SIZE_MB=10
UPLOAD_TMP_DIR=/tmp/kb-uploads
# --- Logging ---
LOG_LEVEL=info # debug | info | warn | error
Every module has a contract (TypeScript interface/Zod schema) and an implementation. No module consumes another module's internal types directly.
Contract Layer (packages/shared/src/)
├── types/
│ ├── note.ts // Note, NoteCreateInput, NoteUpdateInput
│ ├── user.ts // User, UserCreateInput, Session types
│ ├── tag.ts // Tag, TagHierarchy, TagAssignment
│ └── search.ts // SearchQuery, SearchResult, SearchHit
├── schemas/
│ ├── note.schema.ts // Zod validation for note CRUD
│ ├── auth.schema.ts // Zod validation for login/register
│ └── tag.schema.ts // Zod validation for tag operations
└── constants/
├── routes.ts // Centralized route path constants
└── errors.ts // Standardized error codes
Implementation Layer (apps/api/src/)
├── routes/ // Adapters that parse HTTP → call services
├── services/ // Business logic, operate on contract types only
└── models/ // Drizzle schema, DB-specific implementation
Enforcement:
packages/shared is a workspace dependency consumed by both apps/web and apps/api.paths mapping in tsconfig.json resolves @shared/* → packages/shared/src/*.madge in CI (even though CI is excluded, this is enforced at build time via turbo.json dependency graph).no-duplicate-imports + custom rule prevent type divergence.YAGNI (You Aren't Gonna Need It):
Error Handling Strategy:
// Standardized error response format (API contract)
interface ApiError {
code