architecture.md

Architecture

Tech stack, three-layer model, deployment, hosting, Docker, and CI/CD pipeline.

← Back to docs


Technology Stack

Recommended Stack

ComponentTechnologyRationale
Server RuntimeBun3x faster than Node.js for I/O, native TypeScript, built-in SQLite, single binary deploys, excellent DX
Server FrameworkHonoUltra-lightweight (14KB), works everywhere (Bun/Node/Deno/Cloudflare), type-safe middleware, OpenAPI generation
Web FrameworkNext.js 15 (App Router)SSR for SEO (landing/docs), static export for Cloudflare Pages + bundled UI, massive ecosystem
UI Componentsshadcn/ui + Radix UIAccessible, composable, unstyled primitives. Already used in both prototypes.
StylingTailwind CSS 4Utility-first, design token support, excellent with shadcn/ui
MobileReact Native + ExpoCross-platform iOS/Android, shared business logic with web, offline-first capabilities, OTA updates
State ManagementZustandMinimal, 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 StorageFile system (markdown files)The core data IS the files. SQLite indexes them, but the source of truth is always the .md files on disk.
SearchSQLite FTS5 + vector embeddingsFTS5 for instant full-text search, optional embeddings (via sqlite-vec) for semantic search
AuthHybrid (JWT + API Key + Google OAuth)JWT for sessions, API keys for machine access, Google OAuth for cross-device persistence on mino.ink
Real-time SyncWebSocket + Yjs (CRDTs)Conflict-free offline-first sync across devices
AI/LLMModel-agnostic (OpenAI, Anthropic, Google, local)User chooses their provider. Server proxies requests.
Container RegistryGitHub Container Registry (ghcr.io)No pull rate limits, native GitHub Actions integration, free for public images
CI/CDGitHub ActionsBuilds Docker images, pushes to GHCR, deploys frontend to Cloudflare Pages
Web HostingCloudflare PagesFree, 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-updatesWatchtowerMonitors GHCR for new image tags, auto-pulls and restarts containers
Monorepopnpm workspaces + TurborepoShared types, shared components, efficient builds
TestingVitest + PlaywrightFast unit tests, reliable E2E
DocsMintlify or StarlightBeautiful API docs from OpenAPI spec

Why NOT Other Options?

RejectedReason
Go for serverGreat performance, but TypeScript everywhere (server β†’ web β†’ mobile) enables massive code sharing. Type-safe API contracts via shared packages.
Flutter for mobileNo code sharing with the web stack. React Native + Expo means shared components, hooks, and business logic between web and mobile.
PostgreSQLOverkill for a note-taking app. SQLite is embeddable, zero-config, portable, and perfect for self-hosting. One file = your entire index.
MongoDB/NoSQLNotes are files. The index database should be relational (tags, folders, links between notes). SQLite is ideal.
Prisma ORMToo heavy for SQLite. Use drizzle-orm or raw bun:sqlite β€” faster, lighter, better SQLite support.
Vanilla CSSToo much boilerplate for a large consistent design system. Tailwind + design tokens is the pragmatic choice.
DockerHubFree tier has pull rate limits (100/6hr anonymous). GHCR has no limits and integrates natively with GitHub Actions.
VercelGreat for Next.js but unnecessary β€” Cloudflare Pages is free and the frontend is just a static shell.

Deployment & Hosting Architecture

Connection policy:

  • default: relay mode (managed relay connectivity)
  • optional: open-port mode (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:

  1. Generates an Admin API Key (mino_sk_xxxxxxxxxxxx)
  2. Generates a Server ID (unique UUID)
  3. Creates a JWT signing secret
  4. Writes default config.json
  5. Creates /data/notes/ folder structure
  6. Initializes SQLite index (mino.db)
  7. Writes credentials to /data/credentials.json
  8. Starts the API + built-in UI immediately
  9. Exposes GET /api/v1/system/setup with auth details + generated /link URLs

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:

FeatureFree TierSelf-Hosted
StorageLimited (e.g. 100MB / 1000 notes)Unlimited (your disk)
AI AgentBring your own API key onlyInstall Whisper, OCR, local LLMs directly
Transcription (Whisper)Via API key (OpenAI Whisper API)Install locally on server (free, unlimited)
OCRVia API keyInstall Tesseract locally (free, unlimited)
Local AI tools❌ Not availableβœ… If server resources allow
PluginsCore plugins onlyAll plugins + custom plugins
Sandbox / code execution❌ Not availableβœ… Full sandbox container
Cloudflare TunnelN/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):

  1. User creates a free Cloudflare Tunnel in their dashboard
  2. Gets a tunnel token
  3. Adds CF_TUNNEL_TOKEN=xxx to docker-compose environment
  4. Redeploys stack (cloudflared auto-starts when token is present)
  5. Server is accessible at https://random-slug.cfargotunnel.com
  6. 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)

ConcernFiles + SQLitePure Database
PortabilityCopy folder = done. scp, rsync, git.Need pg_dump, migration scripts
Agent compatibilityAgents already understand files and pathsAgents need ORM abstractions
Git integrationNative. Your notes are already a git repo.Need export/import
External editingAny text editor works (VS Code, vim, etc.)Only through the app
Backuptar -czf notes-backup.tar.gz /data/notesDatabase dump + restore
SpeedFTS5 searches millions of rows in <10msAbout the same
RebuildDelete mino.db. Server re-indexes on boot.Data loss risk

Indexing Pipeline

On server start:

  1. Walk the file tree
  2. For each .md file: parse frontmatter, extract title/tags/links, compute content hash
  3. Upsert into SQLite (skip if content hash unchanged)
  4. Build FTS5 index
  5. 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 local all-MiniLM-L6-v2 (sentence-transformers)
  • Storage: sqlite-vec extension 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

  1. Connect: Client opens WebSocket to server
  2. Handshake: Exchange vector clocks / state vectors
  3. Diff: Server sends only the deltas since last sync
  4. Apply: Client applies deltas locally (CRDT merge)
  5. Push: Client sends its local deltas to server
  6. 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.