security.md

Security & Code Quality

Hybrid authentication, credential flow, Cloudflare Tunnel, security hardening, testing, and CI/CD.

← Back to docs


Hybrid Authentication Model

Mino uses a three-tier auth model — no account is ever required:

ā”Œā”€ Auth Decision Flow ────────────────────────────────────────┐
│                                                              │
│  User accesses mino.ink or localhost:3000                    │
│    │                                                         │
│    ā”œā”€ Has Google account linked?                             │
│    │   YES → auto-discover linked servers → JWT session      │
│    │                                                         │
│    ā”œā”€ Has server credentials in localStorage?                │
│    │   YES → connect directly → API key auth                 │
│    │                                                         │
│    └─ Neither?                                               │
│        → Show server-link page (paste URL + API key)         │
│        → OR sign in with Google for convenience              │
│        → OR use the free managed instance                    │
│                                                              │
│  Auth Methods by Context:                                    │
│  ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”   │
│  │ 1. API Key (header: X-Mino-Key)                       │   │
│  │    → Machine access, CLI, MCP, scripts                 │   │
│  │    → Generated on server first boot                    │   │
│  │                                                        │   │
│  │ 2. JWT Bearer Token                                    │   │
│  │    → Web/mobile sessions after credential exchange     │   │
│  │    → Short-lived (15min) + refresh tokens (7 days)     │   │
│  │                                                        │   │
│  │ 3. Google OAuth (mino.ink only)                        │   │
│  │    → Links server credentials to Google account        │   │
│  │    → Enables multi-device server discovery             │   │
│  │    → Google never sees or stores notes                  │   │
│  ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜   │
│                                                              │
│  Each auth method resolves to a User + Permission Set        │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Server-Link Credential Flow

1. Deploy Docker → server auto-bootstraps
2. Server generates: API Key + Server ID + JWT Secret
3. Credentials available at `GET /api/v1/system/setup`
   (includes generated connect URLs for `/link`)

4. User goes to mino.ink (or localhost:3000)
5. Enters Server URL + API Key
   → Option A: stored in localStorage (no account needed)
   → Option B: linked to Google account (multi-device sync)

6. mino.ink calls POST /api/v1/auth/link
   { serverUrl, apiKey }
   → Server validates → returns JWT session token
   → Server marks setupComplete: true

7. All subsequent requests use JWT
   → Auto-refreshed via refresh token
   → If JWT expires and refresh fails → re-enter API key

What Google Sign-In Stores

Stored in Google AccountNOT stored
List of linked server URLsNotes content
Server names ("Personal", "Work")API keys (encrypted at rest)
Last used serverFile system data
User preferences (theme, etc.)LLM API keys

Cloudflare Tunnel (Secure Remote Access)

For servers behind NAT / closed ports, Cloudflare Tunnel provides free, zero-port-exposure remote access:

ā”Œā”€ Your Network ──────────────────────────────────────────────┐
│                                                              │
│  ā”Œā”€ mino-server ─┐  ā”Œā”€ cloudflared ──────────────────────┐ │
│  │  :3000         │  │  Outbound-only connection           │ │
│  │  (no exposed   │──│  to Cloudflare edge                 │ │
│  │   ports)       │  │  TLS 1.3 encrypted                  │ │
│  ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜  ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │
│                                  │                           │
│  NO INBOUND PORTS OPEN           │ outbound :443 only        │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
                                   │
                                   ā–¼
ā”Œā”€ Cloudflare Edge ────────────────────────────────────────────┐
│  https://your-mino.cfargotunnel.com                          │
│  → Proxied to your mino-server via the persistent tunnel     │
│  → DDoS protection, rate limiting, WAF included (free)       │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
                                   │
                                   ā–¼
ā”Œā”€ mino.ink (browser) ────────────────────────────────────────┐
│  API calls → https://your-mino.cfargotunnel.com/api/v1/     │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Setup (via UI):

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

Security properties:

  • Zero inbound ports — only outbound connections
  • Traffic encrypted end-to-end (TLS 1.3)
  • Cloudflare's WAF and DDoS protection (free tier)
  • No IP address exposure
  • Token-revocable — delete the tunnel = instant cutoff

Security Hardening Checklist

MeasureImplementation
Transport encryptionHTTPS everywhere (TLS 1.3). No plain HTTP in production. Cloudflare Tunnel for zero-port exposure.
AuthenticationAPI keys (server-generated, bcrypt-hashed). JWT with short expiry (15min) + refresh tokens (7 days).
AuthorizationRole-based: owner, editor, viewer, agent. Folder-level permissions.
Input validationAll inputs validated via Zod schemas. Markdown content sanitized on render (not on store).
Path traversalAll file paths normalized and validated against the data directory. No ../ escapes.
Rate limitingPer-IP and per-API-key rate limits. Configurable.
CORSStrict origin allowlist (mino.ink + localhost). No * in production.
Content Security PolicyStrict CSP headers on the web app.
Dependency securityAutomated dependency scanning (Dependabot/Renovate). Minimal dependency tree.
Secrets managementAPI keys hashed (bcrypt). JWT secrets auto-generated per server. No secrets in git. Credentials in /data/credentials.json.
Audit loggingLog all write operations with timestamp, user, and action.
End-to-end encryptionOptional client-side encryption for sensitive notes (using Web Crypto API).
SQL injectionParameterized queries only (enforced by drizzle-orm/Bun SQLite API).
XSSMarkdown rendered safely via react-markdown with sanitization. No dangerouslySetInnerHTML.
Plugin sandboxingPlugins run in isolated contexts. No access to server filesystem outside /data/plugins/.
Credential isolationGoogle account stores only server URLs (encrypted). Notes and API keys never leave the server.

Self-Hosted Security

For self-hosted instances:

  1. Network: Bind to 127.0.0.1 by default. Use Cloudflare Tunnel (recommended) or a reverse proxy (Caddy/nginx) for HTTPS.
  2. Firewall: Zero exposed ports with Cloudflare Tunnel. Or expose only port 443 (HTTPS) if using a reverse proxy.
  3. Updates: Watchtower auto-pulls new images from GHCR. One-step updates.
  4. Backups: Built-in backup command or just tar -czf backup.tar.gz /data — it's all files + one SQLite DB.
  5. Credentials: Auto-generated on first boot. Stored in /data/credentials.json. Server never needs manual secret configuration.

Code Organization & Quality

Consistency Guarantees

MechanismPurpose
Shared types package@mino-ink/shared — all TypeScript interfaces shared between server, web, and mobile
Shared API clientType-safe API client generated from OpenAPI spec, used by all clients
Design tokensSingle source of truth for colors, spacing, typography in @mino-ink/design-tokens
Component libraryShared React components in @mino-ink/ui
LintingESLint + Prettier + Oxlint with strict rules
FormattingAutomated via Prettier (enforced in CI)

DRY & Modular Code Practices

  1. No duplicate logic: All markdown parsing, API calls, and validation logic lives in shared packages.
  2. Composable services: The server uses a dependency injection pattern — services are composable and testable.
  3. Feature modules: Each feature (notes, search, agent, plugins) is a self-contained module with its own routes, services, and tests.
  4. No God files: Maximum 500 LOC per file. Split into focused modules.

Testing Strategy

LayerToolWhat's Tested
UnitVitestService logic, utilities, validation, markdown parsing
IntegrationVitest + SupertestAPI endpoints, database operations, file operations
E2EPlaywrightFull user flows in the web app
Mobile E2EDetox (or Maestro)Full user flows in the mobile app
ContractOpenAPI validatorAPI responses match the spec
Performancek6API throughput, search latency

CI/CD Pipeline

# .github/workflows/ci.yml
on: [push, pull_request]

jobs:
  lint:       # ESLint + Prettier + Oxlint
  typecheck:  # tsc --noEmit
  test:       # vitest run
  e2e:        # playwright test
  build:      # Build all packages

  docker:
    # Build multi-arch Docker image (amd64 + arm64)
    # Embed Next.js static export into server image
    # Push to ghcr.io/tomszenessy/mino-server:main + :latest + :vX.Y.Z

  deploy-web:
    # Cloudflare Pages auto-deploy (mino.ink frontend)

# Users: Watchtower on their server auto-pulls new images

Total cost: $0 — GHCR free for public images, GitHub Actions free for open-source, Cloudflare Pages free tier.