refactor (v2): better arch

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-06-07 21:19:54 +02:00
parent 0753f3d256
commit 839308ec19
166 changed files with 8553 additions and 884 deletions

240
README.md
View File

@@ -1,148 +1,188 @@
# K-Notes
A modern, self-hosted note-taking application built with performance, security, and clean architecture in mind.
A self-hosted note-taking engine built in Rust with a strict hexagonal + DDD architecture.
![K-Notes Logo](k-notes-frontend/public/logo.png)
## Features
- **Authentication**: Secure user registration and login.
- **Note Management**: Create, edit, pin, archive, and delete notes.
- **Rich Text**: Markdown support for note content.
- **Version History**: Track changes, view history, note diffs, download versions, and restore previous states.
- **Organization**: Tagging system for easy filtering.
- **Smart Features**: Semantic search and automatically generated related notes using local embeddings.
- **Theme**: Dark and Light mode support.
- **Responsive**: Mobile-friendly UI built with Tailwind CSS.
- **Architecture**:
- **Backend**: Hexagonal Architecture (Domain, Infra, API layers) in Rust.
- **Infrastructure**: Configurable database backends (SQLite, Postgres).
- **Frontend**: Modern React with TypeScript and Vite.
- **Deployment**: Full Docker support with `compose.yml`.
- **Authentication** — JWT-based login and registration (disable registration via `ALLOW_REGISTRATION=false`)
- **Note Management** — create, update, pin, archive, delete, version history
- **Markdown** — content stored and served as Markdown
- **Tagging** — user-scoped tags with get-or-create semantics
- **Search** — full-text search via SQLite FTS5
- **Smart Features** — semantic similarity links between notes using local embeddings (fastembed) and Qdrant; enabled when `QDRANT_URL` is set
- **Export / Import** — portable JSON backup and restore
- **API Docs** — Swagger UI at `/docs`, Scalar at `/scalar`
- **SPA** — React frontend served at `/` by the same process
## Tech Stack
### Backend
- **Language**: Rust
- **Framework**: Axum
- **Database**: SQLite (Default) or Postgres (Supported via feature flag)
- **Vector Database**: Qdrant (for Smart Features)
- **Dependency Injection**: Manual wiring for clear boundaries
| Layer | Technology |
|-------|-----------|
| Language | Rust |
| HTTP | Axum 0.8 |
| Database | SQLite (sqlx + FTS5) |
| Events | NATS JetStream (prod) · in-memory bus (dev) |
| Embeddings | fastembed (AllMiniLML6V2) |
| Vector store | Qdrant |
| Auth | JWT (jsonwebtoken + argon2) |
| API docs | utoipa + Scalar + Swagger UI |
### Frontend
- **Framework**: React + Vite
- **Language**: TypeScript
- **Styling**: Tailwind CSS + Shadcn UI
- **State Management**: TanStack Query (React Query)
| Layer | Technology |
|-------|-----------|
| Framework | React + Vite |
| Language | TypeScript |
| Styling | Tailwind CSS + shadcn/ui |
| State | TanStack Query |
| Package manager | Bun |
## Architecture
The backend follows **Hexagonal Architecture + CQRS**:
```
crates/
domain/ # Entities, value objects, ports (traits)
application/ # Use cases (commands + queries), WorkerService
adapters/
sqlite/ # NoteRepository, TagRepository, UserRepository, LinkRepository
auth/ # Argon2PasswordHasher, JwtValidator, OidcService
nats/ # NatsEventPublisher, NatsEventConsumer (JetStream)
event-publisher-memory/ # In-memory bus for dev/test
event-payload/ # DomainEvent ↔ wire format (JSON)
fastembed/ # EmbeddingGenerator implementation
qdrant/ # VectorStore implementation
wiring/ # Assembles AppContext from env vars
presentation/ # Axum routes, OpenAPI, SPA serving
api-types/ # Request/response DTOs (no domain dependency)
bootstrap/ # HTTP server binary
worker/ # Background event processor binary
```
Dependency direction: `domain ← application ← {presentation, worker}`. Adapters depend on domain only. `wiring` assembles everything.
## Getting Started
### Docker (Recommended)
Run the entire stack with a single command:
### Docker (recommended)
```bash
docker compose up -d --build
docker compose up -d
```
- **Frontend**: http://localhost:8080
- **Backend**: http://localhost:3000
- **App + API**: http://localhost:3000
- **API docs**: http://localhost:3000/docs
The frontend is automatically configured to talk to the backend.
### Local development
### Local Development
#### Prerequisites
#### Backend
- Rust stable (`rustup update stable`)
- Bun (`curl -fsSL https://bun.sh/install | bash`)
1. Navigate to the `notes-api` directory (or root).
2. Set up the environment variables (see `.env.example`).
3. Run the server:
#### Quickstart
```bash
cargo run -p notes-api
cp .env.example .env # edit JWT_SECRET at minimum
make dev # API server on :3000
make dev-frontend # Vite dev server on :5173 (separate terminal)
make dev-worker # background worker (separate terminal, optional)
```
The API server also serves the SPA if you run `make build-frontend` first and `SPA_DIR` points at the dist directory. For active frontend development, use the Vite dev server instead — it hot-reloads and proxies API calls to `:3000`.
By default, this uses the **SQLite** backend.
#### Environment variables
#### Configuration
| Variable | Process | Required | Default | Description |
|----------|---------|----------|---------|-------------|
| `DATABASE_URL` | both | yes | — | SQLite path, e.g. `sqlite:data.db?mode=rwc` |
| `JWT_SECRET` | backend | yes | — | HS256 signing secret — generate with `openssl rand -hex 32` |
| `NATS_URL` | both | no | — | NATS JetStream URL; in-memory bus used if unset |
| `QDRANT_URL` | both | no | — | Enables smart features (semantic links) |
| `ENABLE_EMBEDDINGS` | **worker only** | no | `false` | Set `true` in the worker to load the fastembed model (~150 MB). Leave unset in the backend to save memory. |
| `QDRANT_COLLECTION` | both | no | `notes` | Qdrant collection name |
| `QDRANT_VECTOR_SIZE` | both | no | `384` | Must match the embedding model output dimension |
| `ALLOW_REGISTRATION` | backend | no | `true` | Set `false` to close public registration |
| `SPA_DIR` | backend | no | `k-notes-frontend/dist` | Path to built frontend; set empty for API-only mode |
| `CORS_ORIGINS` | backend | no | — | Comma-separated allowed origins |
| `PORT` | backend | no | `3000` | HTTP listen port |
| `HOST` | backend | no | `0.0.0.0` | HTTP listen address |
| `NATS_MAX_DELIVER` | worker | no | `5` | JetStream dead-letter threshold |
| `SMART_NEIGHBOUR_LIMIT` | worker | no | `10` | Max semantic links per note |
| `SMART_MIN_SIMILARITY` | worker | no | `0.7` | Cosine similarity threshold |
The application is configured via environment variables (or `.env` file):
See `.env.example` for a commented template.
- `ALLOW_REGISTRATION`: Set to `false` to disable new user registration (default: `true`).
- `DATABASE_URL`: Connection string for the database.
- `SESSION_SECRET`: Secret key for session encryption.
- `CORS_ALLOWED_ORIGINS`: Comma-separated list of allowed origins.
## API
**Running with Postgres:**
All endpoints are under `/api/v1`. Full interactive docs at `/docs` (Swagger) or `/scalar` after starting the server.
To use PostgreSQL, build with the `postgres` feature:
```bash
cargo run -p notes-api --no-default-features --features notes-infra/postgres
```
*Note: Ensure your `DATABASE_URL` is set to a valid Postgres connection string.*
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/auth/login` | — | Login, returns JWT |
| POST | `/auth/register` | — | Register (if enabled) |
| GET | `/auth/me` | ✓ | Current user |
| GET | `/config` | — | Server capabilities |
| GET | `/notes` | ✓ | List notes (filter: pinned, archived, tag) |
| POST | `/notes` | ✓ | Create note |
| GET | `/notes/:id` | ✓ | Get note |
| PATCH | `/notes/:id` | ✓ | Update note |
| DELETE | `/notes/:id` | ✓ | Delete note |
| PATCH | `/notes/:id/pin` | ✓ | Pin / unpin |
| PATCH | `/notes/:id/archive` | ✓ | Archive / unarchive |
| GET | `/notes/:id/versions` | ✓ | Version history |
| GET | `/notes/:id/related` | ✓ | Semantically related notes |
| POST | `/notes/:id/tags` | ✓ | Add tag by name |
| DELETE | `/notes/:id/tags/:tag_id` | ✓ | Remove tag |
| GET | `/search?q=` | ✓ | Full-text search |
| GET | `/tags` | ✓ | List tags |
| POST | `/tags` | ✓ | Create tag |
| DELETE | `/tags/:id` | ✓ | Delete tag |
| PATCH | `/tags/:id` | ✓ | Rename tag |
| GET | `/export` | ✓ | Export all data as JSON |
| POST | `/import` | ✓ | Import from backup JSON |
**Feature Flags (Smart Features):**
The application includes "Smart Features" (semantic search, related notes) enabled by default. These require `fastembed`, `qdrant-client`, and `async-nats`.
To build/run **without** smart features (for faster compilation or lighter deployment):
## Deployment
```bash
cargo run -p notes-api --no-default-features --features sqlite
# Build and push image
make deploy
# Or manually
IMAGE=your-registry/k-notes:latest make docker-build docker-push
```
#### Frontend
The `CMD` in the Dockerfile starts `bootstrap` (HTTP server). Run `./worker` as a separate container or process for background event processing.
1. Navigate to `k-notes-frontend`.
2. Install dependencies:
```bash
bun install
```
3. Run the dev server:
```bash
bun dev
```
## Database Architecture
The backend follows a Hexagonal Architecture (Ports and Adapters). The `notes-domain` crate defines the repository capabilities (Ports), and `notes-infra` implements them (Adapters).
### Supported Databases
- **SQLite**: Fully implemented (default). Ideal for single-instance, self-hosted deployments.
- **Postgres**: Structure is in place (via feature flag), ready for implementation.
### Extending Database Support
To add a new database (e.g., MySQL), follow these steps:
1. **Dependencies**: Add the driver to `notes-infra/Cargo.toml` (e.g., `sqlx` with `mysql` feature) and create a feature flag.
2. **Configuration**: Update `DatabaseConfig` in `notes-infra/src/db.rs` to handle the new connection URL scheme and connection logic in `create_pool`.
3. **Repository Implementation**:
- Implement `NoteRepository`, `TagRepository`, and `UserRepository` traits for the new database in `notes-infra`.
4. **Factory Integration**:
- Update `notes-infra/src/factory.rs` to include a builder for the new repositories.
- Update `build_database_pool` and repository `build_*` functions to support the new database type match arm.
5. **Migrations**:
- Add migration files in `migrations/<db_type>`.
- Update `run_migrations` in `db.rs` to execute them.
This design ensures the `notes-api` layer remains completely agnostic to the underlying database technology.
**Docker Compose** volumes to mount:
- `/app/data` — SQLite database file
- `/app/data/model-cache` — fastembed model cache (avoids re-download on restart)
## Project Structure
```
├── notes-api # API Interface (Axum, HTTP routes)
├── notes-domain # Core Business Logic (Entities, Services, Ports)
├── notes-infra # Infrastructure (Database adapters, Repositories)
├── k-notes-frontend # React Frontend Application
├── migrations # SQLx Database Migrations
└── compose.yml # Docker Composition
crates/ # New architecture (active)
adapters/ # Infrastructure adapters
api-types/ # HTTP DTOs
application/ # Use cases + WorkerService
bootstrap/ # API server binary
domain/ # Core domain
presentation/ # Axum + OpenAPI + SPA
wiring/ # Dependency assembly
worker/ # Event worker binary
k-notes-frontend/ # React SPA
migrations/ # Legacy migration files (see crates/adapters/sqlite/migrations/)
notes-api/ ⚠ DEPRECATED — will be removed in a future release
notes-domain/ ⚠ DEPRECATED — will be removed in a future release
notes-infra/ ⚠ DEPRECATED — will be removed in a future release
notes-worker/ ⚠ DEPRECATED — will be removed in a future release
```
> **Note**: The `notes-*` directories contain the original monolithic implementation and are kept for reference only. They are excluded from the workspace and are not built. All active development happens in `crates/`.
## License
MIT
MIT — Copyright (c) 2025-2026 Gabriel Kaszewski