security.md
Security & Code Quality
Hybrid authentication, credential flow, Cloudflare Tunnel, security hardening, testing, and CI/CD.
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 Account | NOT stored |
|---|---|
| List of linked server URLs | Notes content |
| Server names ("Personal", "Work") | API keys (encrypted at rest) |
| Last used server | File 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):
- User creates a free Cloudflare Tunnel in their dashboard
- Gets a tunnel token
- Adds it to docker-compose:
CF_TUNNEL_TOKEN=xxx - Redeploys stack (cloudflared auto-starts when token is present)
- 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
| Measure | Implementation |
|---|---|
| Transport encryption | HTTPS everywhere (TLS 1.3). No plain HTTP in production. Cloudflare Tunnel for zero-port exposure. |
| Authentication | API keys (server-generated, bcrypt-hashed). JWT with short expiry (15min) + refresh tokens (7 days). |
| Authorization | Role-based: owner, editor, viewer, agent. Folder-level permissions. |
| Input validation | All inputs validated via Zod schemas. Markdown content sanitized on render (not on store). |
| Path traversal | All file paths normalized and validated against the data directory. No ../ escapes. |
| Rate limiting | Per-IP and per-API-key rate limits. Configurable. |
| CORS | Strict origin allowlist (mino.ink + localhost). No * in production. |
| Content Security Policy | Strict CSP headers on the web app. |
| Dependency security | Automated dependency scanning (Dependabot/Renovate). Minimal dependency tree. |
| Secrets management | API keys hashed (bcrypt). JWT secrets auto-generated per server. No secrets in git. Credentials in /data/credentials.json. |
| Audit logging | Log all write operations with timestamp, user, and action. |
| End-to-end encryption | Optional client-side encryption for sensitive notes (using Web Crypto API). |
| SQL injection | Parameterized queries only (enforced by drizzle-orm/Bun SQLite API). |
| XSS | Markdown rendered safely via react-markdown with sanitization. No dangerouslySetInnerHTML. |
| Plugin sandboxing | Plugins run in isolated contexts. No access to server filesystem outside /data/plugins/. |
| Credential isolation | Google account stores only server URLs (encrypted). Notes and API keys never leave the server. |
Self-Hosted Security
For self-hosted instances:
- Network: Bind to
127.0.0.1by default. Use Cloudflare Tunnel (recommended) or a reverse proxy (Caddy/nginx) for HTTPS. - Firewall: Zero exposed ports with Cloudflare Tunnel. Or expose only port 443 (HTTPS) if using a reverse proxy.
- Updates: Watchtower auto-pulls new images from GHCR. One-step updates.
- Backups: Built-in backup command or just
tar -czf backup.tar.gz /dataā it's all files + one SQLite DB. - Credentials: Auto-generated on first boot. Stored in
/data/credentials.json. Server never needs manual secret configuration.
Code Organization & Quality
Consistency Guarantees
| Mechanism | Purpose |
|---|---|
| Shared types package | @mino-ink/shared ā all TypeScript interfaces shared between server, web, and mobile |
| Shared API client | Type-safe API client generated from OpenAPI spec, used by all clients |
| Design tokens | Single source of truth for colors, spacing, typography in @mino-ink/design-tokens |
| Component library | Shared React components in @mino-ink/ui |
| Linting | ESLint + Prettier + Oxlint with strict rules |
| Formatting | Automated via Prettier (enforced in CI) |
DRY & Modular Code Practices
- No duplicate logic: All markdown parsing, API calls, and validation logic lives in shared packages.
- Composable services: The server uses a dependency injection pattern ā services are composable and testable.
- Feature modules: Each feature (notes, search, agent, plugins) is a self-contained module with its own routes, services, and tests.
- No God files: Maximum 500 LOC per file. Split into focused modules.
Testing Strategy
| Layer | Tool | What's Tested |
|---|---|---|
| Unit | Vitest | Service logic, utilities, validation, markdown parsing |
| Integration | Vitest + Supertest | API endpoints, database operations, file operations |
| E2E | Playwright | Full user flows in the web app |
| Mobile E2E | Detox (or Maestro) | Full user flows in the mobile app |
| Contract | OpenAPI validator | API responses match the spec |
| Performance | k6 | API 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.