architecture.md
Architecture
Tech stack, three-layer model, deployment, hosting, Docker, and CI/CD pipeline.
Technology Stack
Recommended Stack
| Component | Technology | Rationale |
|---|---|---|
| Server Runtime | Bun | 3x faster than Node.js for I/O, native TypeScript, built-in SQLite, single binary deploys, excellent DX |
| Server Framework | Hono | Ultra-lightweight (14KB), works everywhere (Bun/Node/Deno/Cloudflare), type-safe middleware, OpenAPI generation |
| Web Framework | Next.js 15 (App Router) | SSR for SEO (landing/docs), static export for Cloudflare Pages + bundled UI, massive ecosystem |
| UI Components | shadcn/ui + Radix UI | Accessible, composable, unstyled primitives. Already used in both prototypes. |
| Styling | Tailwind CSS 4 | Utility-first, design token support, excellent with shadcn/ui |
| Mobile | React Native + Expo | Cross-platform iOS/Android, shared business logic with web, offline-first capabilities, OTA updates |
| State Management | Zustand | Minimal, unopinionated, works well with React and React Native |
| Database (Index) | SQLite (via Bun built-in) | Zero-config, embedded, incredibly fast for read-heavy workloads, portable, FTS5 for full-text search |
| Data Storage | File system (markdown files) | The core data IS the files. SQLite indexes them, but the source of truth is always the .md files on disk. |
| Search | SQLite FTS5 + vector embeddings | FTS5 for instant full-text search, optional embeddings (via sqlite-vec) for semantic search |
| Auth | Hybrid (JWT + API Key + Google OAuth) | JWT for sessions, API keys for machine access, Google OAuth for cross-device persistence on mino.ink |
| Real-time Sync | WebSocket + Yjs (CRDTs) | Conflict-free offline-first sync across devices |
| AI/LLM | Model-agnostic (OpenAI, Anthropic, Google, local) | User chooses their provider. Server proxies requests. |
| Container Registry | GitHub Container Registry (ghcr.io) | No pull rate limits, native GitHub Actions integration, free for public images |
| CI/CD | GitHub Actions | Builds Docker images, pushes to GHCR, deploys frontend to Cloudflare Pages |
| Web Hosting | Cloudflare Pages | Free, global CDN, static Next.js export, zero-config deploys |
| Tunnel (optional) | Cloudflare Tunnel (cloudflared) | Free, zero-port-exposure remote access to self-hosted servers |
| Auto-updates | Watchtower | Monitors GHCR for new image tags, auto-pulls and restarts containers |
| Monorepo | pnpm workspaces + Turborepo | Shared types, shared components, efficient builds |
| Testing | Vitest + Playwright | Fast unit tests, reliable E2E |
| Docs | Mintlify or Starlight | Beautiful API docs from OpenAPI spec |
Why NOT Other Options?
| Rejected | Reason |
|---|---|
| Go for server | Great performance, but TypeScript everywhere (server β web β mobile) enables massive code sharing. Type-safe API contracts via shared packages. |
| Flutter for mobile | No code sharing with the web stack. React Native + Expo means shared components, hooks, and business logic between web and mobile. |
| PostgreSQL | Overkill for a note-taking app. SQLite is embeddable, zero-config, portable, and perfect for self-hosting. One file = your entire index. |
| MongoDB/NoSQL | Notes are files. The index database should be relational (tags, folders, links between notes). SQLite is ideal. |
| Prisma ORM | Too heavy for SQLite. Use drizzle-orm or raw bun:sqlite β faster, lighter, better SQLite support. |
| Vanilla CSS | Too much boilerplate for a large consistent design system. Tailwind + design tokens is the pragmatic choice. |
| DockerHub | Free tier has pull rate limits (100/6hr anonymous). GHCR has no limits and integrates natively with GitHub Actions. |
| Vercel | Great for Next.js but unnecessary β Cloudflare Pages is free and the frontend is just a static shell. |
Deployment & Hosting Architecture
Connection policy:
- default:
relaymode (managed relay connectivity) - optional:
open-portmode (direct public endpoint) - relay deployment details:
docs/relay.md
Overview
ββ mino.ink (Cloudflare Pages, FREE) ββββββββββββββββββββββββββ
β Static Next.js export β just a UI shell β
β Auth: optional Google sign-in (persists linked servers) β
β OR: just paste server credentials (localStorage only) β
β OR: use the free-tier managed instance (limited) β
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββ΄βββββββββββββββ
β β
Direct HTTPS Cloudflare Tunnel (free)
(port forwarded) (zero ports exposed)
β β
ββββββββββββββββ¬βββββββββββββββ
βΌ
ββ User's Server (Docker, self-hosted) ββββββββββββββββββββββββ
β ghcr.io/tomszenessy/mino-server:main (default) β
β β
β ββ Hono API server (:3000) β
β ββ Built-in Web UI (same as mino.ink, served at /) β
β ββ Agent Runtime (LLM, tools, plugins) β
β ββ SQLite index + file watcher β
β ββ Plugin host (install/load/update at runtime) β
β ββ Sandbox (optional, for code execution / local AI tools) β
β ββ /data/ (notes, config, credentials, SQLite) β
β β
β Optional sidecars: β
β ββ cloudflared (Cloudflare Tunnel for remote access) β
β ββ watchtower (auto-updates from GHCR) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Docker Compose (One-Paste for Portainer)
services:
mino:
image: ghcr.io/tomszenessy/mino-server:${MINO_IMAGE_TAG:-main}
volumes:
- mino-data:/data
ports:
- "${MINO_PORT_BIND:-0.0.0.0}:${MINO_PORT:-3000}:3000"
restart: unless-stopped
# No environment variables needed β auto-bootstraps on first run
# Optional: Cloudflare Tunnel for remote access (free, no open ports)
cloudflared:
image: cloudflare/cloudflared:latest
entrypoint: ["/bin/sh"]
command:
- -c
- |
if [ -n "$${TUNNEL_TOKEN:-}" ]; then
exec cloudflared tunnel --no-autoupdate run --token "$${TUNNEL_TOKEN}"
fi
exec tail -f /dev/null
environment:
- TUNNEL_TOKEN=${CF_TUNNEL_TOKEN:-}
depends_on:
- mino
restart: unless-stopped
# Optional: auto-updates from GHCR
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_POLL_INTERVAL=86400 # Check daily
profiles: ["autoupdate"]
volumes:
mino-data:
Auto-Bootstrap (Zero Console Setup)
On first boot, the server detects /data is empty and bootstraps automatically:
- Generates an Admin API Key (
mino_sk_xxxxxxxxxxxx) - Generates a Server ID (unique UUID)
- Creates a JWT signing secret
- Writes default
config.json - Creates
/data/notes/folder structure - Initializes SQLite index (
mino.db) - Writes credentials to
/data/credentials.json - Starts the API + built-in UI immediately
- Exposes
GET /api/v1/system/setupwith auth details + generated/linkURLs
No wizard. No terminal interaction. No environment variables needed. User opens Portainer β deploys β opens /api/v1/system/setup β clicks generated /link URL β done.
Server-Link Flow (Connecting mino.ink to a Server)
1. User deploys Docker β server auto-bootstraps
2. Server returns setup payload at /api/v1/system/setup
3. User opens one of the generated `/link?serverUrl=...&apiKey=...` URLs
4. Web client calls:
a. POST {serverUrl}/api/v1/auth/verify
b. POST {serverUrl}/api/v1/auth/link
5. Web client stores linked profile locally and redirects to `/workspace?profile=<id>`
6. All future API calls: Browser (mino.ink/test.mino.ink/local UI) β User's server directly
Built-in Web UI
The server bundles the same web interface as mino.ink:
http://localhost:3000/ β Full web UI (identical to mino.ink)
http://localhost:3000/link β Dedicated link handler
http://localhost:3000/workspace β Workspace shell
http://localhost:3000/docs β Docs explorer (`/docs` + `/docstart`)
http://localhost:3000/api/v1/system/setup β First-run setup payload
http://localhost:3000/api/v1/ β REST API
http://localhost:3000/ws β WebSocket
Build process: GitHub Actions builds the Next.js frontend as a static export β the static files are embedded into the Docker image β Hono serves them at /.
This means:
- Remote server: User accesses via mino.ink β API calls go to their server
- Local server: User opens
http://localhost:3000β full UI + API in one - Air-gapped: Everything works offline with no external dependencies
Free Tier (mino.ink Managed Instance)
Users who don't want to self-host get a free limited instance automatically:
| Feature | Free Tier | Self-Hosted |
|---|---|---|
| Storage | Limited (e.g. 100MB / 1000 notes) | Unlimited (your disk) |
| AI Agent | Bring your own API key only | Install Whisper, OCR, local LLMs directly |
| Transcription (Whisper) | Via API key (OpenAI Whisper API) | Install locally on server (free, unlimited) |
| OCR | Via API key | Install Tesseract locally (free, unlimited) |
| Local AI tools | β Not available | β If server resources allow |
| Plugins | Core plugins only | All plugins + custom plugins |
| Sandbox / code execution | β Not available | β Full sandbox container |
| Cloudflare Tunnel | N/A (already hosted) | β Optional sidecar |
| Custom domain | β | β Your own domain |
The server auto-detects available resources (CPU, RAM, GPU) and enables/disables features accordingly. For example, if a self-hosted server has a GPU, it can run Whisper locally for free transcription instead of requiring an API key.
Cloudflare Tunnel (Free Remote Access)
For users whose server ports are closed (behind NAT, no port forwarding):
- User creates a free Cloudflare Tunnel in their dashboard
- Gets a tunnel token
- Adds
CF_TUNNEL_TOKEN=xxxto docker-compose environment - Redeploys stack (cloudflared auto-starts when token is present)
- Server is accessible at
https://random-slug.cfargotunnel.com - Zero ports exposed, traffic encrypted end-to-end
CI/CD Pipeline (All Free)
GitHub repo (TomSzenessy/MinoAI)
β
ββ On push to main / create version tag
β β
β ββ GitHub Actions
β β ββ Lint + typecheck + test
β β ββ Build multi-arch Docker image (amd64 + arm64)
β β ββ Build Next.js static export β embed in Docker image
β β ββ Push to ghcr.io/tomszenessy/mino-server:main + :latest + :vX.Y.Z
β β
β ββ Cloudflare Pages (auto-deploy)
β ββ Builds + deploys mino.ink frontend (static site)
β
ββ On user's server
ββ Watchtower detects new ghcr.io tag β pulls + restarts β zero-downtime update
Total cost: $0. GHCR free for public images, GitHub Actions free for open-source, Cloudflare Pages free tier, Watchtower is just a container.
Monorepo Structure
mino/
βββ packages/
β βββ shared/ # Shared types, utils, API contracts
β β βββ types/ # TypeScript types (Note, Folder, User, etc.)
β β βββ api-client/ # Type-safe API client (used by web + mobile)
β β βββ markdown/ # Markdown parsing/rendering utilities
β β βββ design-tokens/ # CSS variables, Tailwind preset
β βββ ui/ # Shared React components (works in web + RN)
β βββ primitives/ # Button, Input, Card, etc.
β βββ features/ # Editor, Sidebar, NoteList, etc.
βββ apps/
β βββ server/ # Bun + Hono API server (+ bundled web UI)
β βββ web/ # Next.js web application (mino.ink + bundled UI)
β βββ mobile/ # React Native + Expo app
βββ tools/
β βββ mcp-server/ # MCP tool server for AI agents
β βββ cli/ # CLI tool for server management
βββ docker/ # Dockerfiles, docker-compose.yml
β βββ Dockerfile # Multi-stage: build web β embed in server image
β βββ docker-compose.yml # One-paste Portainer deployment
βββ docs/ # Documentation (this folder)
βββ pnpm-workspace.yaml
βββ turbo.json
βββ README.md
βββ MASTER_PLAN.md
Three-Layer Architecture
ββ Layer 1: INTERFACES βββββββββββββββββββββββββββββββββββββββββββ
β mino.ink β Built-in UI β Mobile β CLI β MCP β API Clients β
β (CF Pages) (localhost) (Expo) (Bun) (SDK) (curl/fetch) β
ββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β HTTPS + WebSocket
βΌ
ββ Layer 2: MINO SERVER βββββββββββββββββββββββββββββββββββββββββββ
β β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ β
β β HTTP Router β β WebSocket β β Agent Runtime β β
β β (Hono) β β (ws + Yjs) β β (LLM + Tools) β β
β ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββββ¬ββββββββββ β
β β β β β
β ββββββββ΄ββββββββββββββββββ΄βββββββββββββββββββββ΄ββββββββββ β
β β SERVICE LAYER β β
β β NoteService β FolderService β SearchService β Auth β β
β β PluginService β SandboxService β ResourceDetector β β
β ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββ β
β β DATA LAYER β β
β β FileManager (R/W .md) β IndexDB (SQLite FTS+Vec) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β STATIC FILES β β
β β Built-in Web UI (Next.js static export, served at /) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
ββ Layer 3: STORAGE βββββββββββββββββββββββββββββββββββββββββββββββ
π /data/notes/**/*.md (source of truth)
π /data/assets/** (images, attachments)
π /data/plugins/** (installed plugins)
π /data/mino.db (SQLite index)
π /data/config.json (server config)
π /data/credentials.json (auto-generated on first boot)
Data Flow: How a Note Gets Created
sequenceDiagram
participant C as Client (Web/Mobile/Agent)
participant S as Mino Server
participant F as File System
participant I as Index (SQLite)
C->>S: POST /api/v1/notes {path, content}
S->>S: Validate auth + permissions
S->>F: Write /data/notes/path.md
S->>I: INSERT INTO notes_fts (+ embeddings if enabled)
S->>S: Broadcast via WebSocket
S->>C: 201 Created {note metadata}
Multi-Server Architecture
Users can link multiple independent Mino servers to one Google account:
Server A (Personal) Server B (Work) Server C (Shared Team)
βββ ~/personal-notes/ βββ ~/work-notes/ βββ /shared/team-notes/
β β β
Docker on home NAS Docker on work server Docker on cloud VPS
β β β
ββββ mino.ink (server picker β switch between linked servers) βββββββββββ
β Sign in with Google β see all linked servers β select one β connectedβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Data Storage & Indexing Strategy
The Hybrid Approach
Source of truth: .md files on disk
Index for speed: SQLite database
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β FILE SYSTEM (source of truth) β
β β
β /data/notes/ β
β βββ Projects/ β
β β βββ Alpha/ β
β β β βββ architecture.md β
β β β βββ meeting-2026-02-11.md β
β β βββ Beta/ β
β β βββ roadmap.md β
β βββ Daily/ β
β βββ 2026-02-11.md β
β βββ 2026-02-10.md β
β β
β /data/assets/ β
β βββ images/ β
β βββ attachments/ β
β β
β /data/plugins/ β
β βββ web-search/ β
β βββ whisper-local/ β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β File watcher + on-demand re-index
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β SQLite INDEX (derived, rebuildable)β
β β
β notes table: β
β path, title, content_hash, tags, created, β
β modified, word_count, frontmatter_json β
β β
β notes_fts (FTS5 virtual table): β
β title, content, tags β
β β
β notes_vec (vector table, optional): β
β path, embedding (1536-dim float array) β
β β
β links table: β
β source_path, target_path β
β β
β tags table: β
β tag, note_path β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Why Files + SQLite (Not a Database)
| Concern | Files + SQLite | Pure Database |
|---|---|---|
| Portability | Copy folder = done. scp, rsync, git. | Need pg_dump, migration scripts |
| Agent compatibility | Agents already understand files and paths | Agents need ORM abstractions |
| Git integration | Native. Your notes are already a git repo. | Need export/import |
| External editing | Any text editor works (VS Code, vim, etc.) | Only through the app |
| Backup | tar -czf notes-backup.tar.gz /data/notes | Database dump + restore |
| Speed | FTS5 searches millions of rows in <10ms | About the same |
| Rebuild | Delete mino.db. Server re-indexes on boot. | Data loss risk |
Indexing Pipeline
On server start:
- Walk the file tree
- For each
.mdfile: parse frontmatter, extract title/tags/links, compute content hash - Upsert into SQLite (skip if content hash unchanged)
- Build FTS5 index
- Optionally generate embeddings (async, background)
This takes <2 seconds for 10,000 notes on modern hardware.
Embedding Strategy
For semantic search:
- Model:
text-embedding-3-small(OpenAI) or localall-MiniLM-L6-v2(sentence-transformers) - Storage:
sqlite-vecextension for SQLite vector similarity search - Chunking: Split notes at heading boundaries. Each heading section = one embedding.
- Updates: Re-embed only changed files (compare content hash)
- Cost: ~$0.02 per 1,000 notes (OpenAI), free for local models
Offline-First & Sync Strategy
CRDT-Based Sync (Yjs)
Device A (offline) Mino Server Device B (online)
β β β
βββ Edit note βββΊ β β
β (queued) β β
β βββ Edit same βββΊ β
β β note β
βββ Come online βββΊ β β
β Send Yjs update β β
β βββ Merge βββΊ β
β β (CRDT) β
ββββ Merged state ββ βββ Merged state βββ
β β β
Sync Protocol
- Connect: Client opens WebSocket to server
- Handshake: Exchange vector clocks / state vectors
- Diff: Server sends only the deltas since last sync
- Apply: Client applies deltas locally (CRDT merge)
- Push: Client sends its local deltas to server
- Continuous: WebSocket stays open for real-time updates
Conflict Resolution
CRDTs guarantee that all devices converge to the same state, regardless of the order edits arrive. No manual conflict resolution needed.
For the rare case of irreconcilable conflicts (e.g., one device deleted a note while another edited it), the "edit wins" policy is applied β deletions are soft-deletes that can be recovered.