A local-first Markdown knowledge vault for your AI-generated notes.
Build from Source | Deploy Server | Documentation
- Local-first storage — Your Markdown stays on your machine (or your own cloud storage)
- Bulk import — Import hundreds of Markdown files with scan preview, duplicate detection, and retry
- Full-text search — SQLite FTS5 powers instant search across titles, content, and metadata
- Smart organization — Collections, tags, review queue, and AI-powered topic grouping
- Optional AI — Summarize, question, and extract insights (configure your own AI provider)
- Backup & restore — Manual backup, download, delete, and confirmed restore flows
- Desktop & server — Desktop build target or self-hosted server
-
Build a local desktop package:
make desktop-build
-
Install and launch the application
-
On first run, create an admin account
-
Import your Markdown files: System → Import → Select Directory
-
Start searching and organizing!
See Desktop Guide for detailed instructions.
-
Build or unpack the server binary:
make server-build-v1 cp dist/server/markdown-vault ./cloud-vault
-
Create config file:
cp config.example.toml config.toml # Edit config.toml as needed -
Run the server:
./cloud-vault
-
Open http://localhost:8080 and create an admin account
See Server Deployment Guide for Docker, systemd, and HTTPS setup.
Configuration is loaded from config.toml, .env file, or environment variables (in that order, env vars take precedence).
Key options:
server.addr— HTTP listen address (default::8080)database.path— SQLite database path (default:./data/app.db)storage.provider—localorqiniu(default:local)auth.enabled— Enable authentication (default:true)ai.enabled— Enable AI features (default:false)
See Configuration Guide for all options.
- Navigate to System → Import
- Select a server-discovered directory under
IMPORTER_SAFE_ROOT - Review scan preview (file count, size, duplicates)
- Click "Start Import"
- Monitor progress and retry failures
See Import Guide for advanced options.
- Search — Use the search bar to find documents by title, content, or metadata
- Collections — Group related documents into collections
- Tags — Add tags for flexible categorization
- Review queue — Process unprocessed imports
- AI topics — Get AI-powered topic suggestions (if enabled)
See Search Guide and Organize Guide.
AI features are disabled by default. To enable:
-
Configure an OpenAI-compatible provider in
config.toml:[ai] enabled = true provider = "openai_compatible" base_url = "https://api.openai.com/v1" api_key = "your-api-key" model = "gpt-4"
-
Use AI features:
- Summarize documents
- Ask questions about selected documents
- Extract prompts from conversations
- Generate new documents from AI output
Privacy: Only documents you explicitly select are sent to the AI provider.
See AI Guide for details.
curl -X POST http://localhost:8080/api/v1/system/backup \
-H 'Content-Type: application/json' \
-b cookie.txtOr use the web UI: System → Backup → Create Backup
./cloud-vault restore --file backups/backup-20260530-120000.zip --data-dir ./dataIf you encounter issues, export a diagnostic bundle:
- Navigate to Settings → Diagnostics
- Click "Export Diagnostic Bundle"
- Download the generated zip file
- Share with support (if applicable)
Privacy: Diagnostic bundles include redacted config, logs, and system info. They do not include Markdown content, database files, API keys, session tokens, or passwords.
See Diagnostics Guide.
- LocalStorage mode — All data stays on your machine
- QiniuStorage mode — Data uploaded to your own Qiniu bucket (you control access)
- AI features — Optional, only send documents you explicitly select
- Session cookies — HttpOnly flag, secure flag recommended for production
- Diagnostic bundles — Exclude Markdown content and secrets
See Privacy Policy and Security Guide.
- Go 1.26+
- Node.js 20+ and pnpm
- (Optional) Wails CLI for desktop builds:
go install github.com/wailsapp/wails/v2/cmd/wails@latest
# Clone repository
git clone <your-plumego-repository-url>
cd plumego/use-cases/cloud-vault
# Build server
make build
# Run server
./bin/markdown-vault
# Build desktop app (requires Wails CLI)
make desktop-build# Go tests
make test
# Frontend tests
cd web && pnpm test
# E2E tests (requires Playwright)
make test-e2eSee Development Guide.
- V1.0 does not support multi-user real-time collaboration
- V1.0 does not support cloud sync accounts
- Public installers and release feed URLs must be supplied by the release owner
- Auto-update check only notifies, does not download or silently install
- AI features require user-configured model provider
- Approximate duplicate and topic detection are suggestions, may not be fully accurate
- QiniuStorage requires user to manage bucket permissions and backup strategy
- macOS/Windows installers may show security warnings if not signed (see Installation Guide)
The sections below contain detailed developer documentation for all versions.
| Layer | Technology |
|---|---|
| Backend | Go 1.26 + Plumego |
| Database | SQLite (modernc.org/sqlite) |
| Object Storage | Local filesystem / Qiniu Kodo |
| ID Generation | ULID (oklog/ulid/v2) |
| Markdown Parsing | goldmark |
| Frontend | React + Vite + TypeScript |
| Editor | CodeMirror 6 |
| Styling | Tailwind CSS + shadcn/ui |
| Deployment | Single Go binary with embedded frontend |
- Go 1.26+
- Node.js 20+ and pnpm
go run ./cmd/serverServer starts at http://localhost:8080. The API is available at /api/v1/.
Terminal 1 — Backend:
go run ./cmd/serverTerminal 2 — Frontend dev server:
make dev
# or: cd web && pnpm devThe Vite dev server proxies /api/* to :8080. Open http://localhost:5173.
Configuration is loaded from environment variables and an optional .env file (default: .env).
cp env.example .env
# edit .env as needed| Variable | Default | Description |
|---|---|---|
APP_ADDR |
:8080 |
HTTP listen address |
DB_PATH |
./data/app.db |
SQLite database path |
STORAGE_PROVIDER |
local |
local or qiniu |
LOCAL_ROOT |
./data/objects |
Root for local object storage |
APP_MAX_UPLOAD_SIZE_MB |
10 |
Maximum upload size in MB |
QINIU_ACCESS_KEY |
— | Qiniu access key |
QINIU_SECRET_KEY |
— | Qiniu secret key |
QINIU_BUCKET |
— | Qiniu bucket name |
QINIU_DOMAIN |
— | Qiniu CDN domain (e.g. https://example.com) |
QINIU_REGION |
z0 |
Qiniu region: z0 华东, z1 华北, z2 华南, na0 北美, as0 新加坡 |
QINIU_USE_HTTPS |
true |
Whether to use HTTPS for Qiniu API calls |
AUTH_ENABLED |
false |
Enable authentication (see V0.7) |
AUTH_SESSION_TTL_HOURS |
720 |
Session TTL in hours (default 30 days) |
AUTH_COOKIE_NAME |
cv_session |
Session cookie name |
AUTH_SECURE_COOKIE |
false |
Use Secure flag on cookies (HTTPS only) |
AUTH_MAX_LOGIN_FAILURES |
5 |
Max failures before lockout |
AUTH_PASSWORD_MIN_LENGTH |
12 |
Minimum password length (≥ 8) |
AUTH_BOOTSTRAP_ADMIN_ENABLED |
false |
Auto-create admin on first startup |
AUTH_BOOTSTRAP_ADMIN_USERNAME |
— | Bootstrap admin username |
AUTH_BOOTSTRAP_ADMIN_EMAIL |
— | Bootstrap admin email |
AUTH_BOOTSTRAP_ADMIN_PASSWORD |
— | Bootstrap admin password (set via env for security) |
No configuration needed. Files are stored under ./data/objects/:
./data/objects/docs/{doc_id}/current.md
./data/objects/docs/{doc_id}/versions/000001.md
- Create a bucket in Qiniu Console.
- Set the following variables (in
.envor environment):
STORAGE_PROVIDER=qiniu
QINIU_ACCESS_KEY=your-ak
QINIU_SECRET_KEY=your-sk
QINIU_BUCKET=your-bucket
QINIU_DOMAIN=https://your-cdn-domain.com
QINIU_REGION=z0Note: For private buckets,
Getuses signed URLs valid for 1 hour. Public buckets work out of the box.
All endpoints are under /api/v1. Successful responses use {"data": ...}, errors use {"error": {"code": "...", "message": "..."}}.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/health |
Health check |
| GET | /api/v1/documents |
List documents (?q=, ?limit=, ?offset=) |
| POST | /api/v1/documents |
Create document |
| GET | /api/v1/documents/:id |
Get document with content |
| PUT | /api/v1/documents/:id |
Update document |
| DELETE | /api/v1/documents/:id |
Soft-delete document |
| GET | /api/v1/documents/:id/versions |
List document versions |
| GET | /api/v1/documents/:id/versions/:version |
Get specific version content |
| POST | /api/v1/auth/login |
Login (public, returns session cookie) |
| POST | /api/v1/auth/logout |
Logout and revoke session (protected) |
| POST | /api/v1/auth/setup |
Create first admin user (public, one-time) |
| GET | /api/v1/auth/status |
Check if system is initialized (public) |
| GET | /api/v1/auth/me |
Get current user profile (protected) |
| PUT | /api/v1/auth/me |
Update user profile (protected) |
| POST | /api/v1/auth/change-password |
Change password (protected) |
| GET | /api/v1/auth/sessions |
List active sessions (protected) |
| DELETE | /api/v1/auth/sessions/:id |
Revoke specific session (protected) |
| POST | /api/v1/auth/sessions/revoke-all |
Revoke all other sessions (protected) |
| GET | /api/v1/auth/security-events |
List security events (protected) |
| POST | /api/v1/system/backup |
Create backup (protected) |
| GET | /api/v1/system/backups |
List backups (protected) |
| GET | /api/v1/system/backups/:name/download |
Download backup (protected) |
| DELETE | /api/v1/system/backups/:name |
Delete backup (protected) |
| POST | /api/v1/system/restore |
Restore from backup (protected, requires confirm=RESTORE) |
| GET | /api/v1/system/settings |
Get non-sensitive system settings (protected) |
Note: Protected endpoints require a valid session cookie. See V0.7 documentation for authentication details.
PUT /api/v1/documents/:id requires base_version matching the current version. If the document was updated concurrently, a 409 Conflict is returned:
{"error": {"code": "DOCUMENT_VERSION_CONFLICT", "message": "document has been updated by another session"}}make build
# or step by step:
make web-build # builds frontend into internal/web/static/
make server-build # embeds frontend and compiles Go binary → bin/markdown-vault
./bin/markdown-vault| Table | Purpose |
|---|---|
documents |
Document metadata, current version pointer |
document_versions |
Version history (storage key + hash per version) |
tags |
Tag definitions (reserved for V0.2) |
document_tags |
Document↔tag associations (reserved for V0.2) |
sync_jobs |
Background sync job queue (reserved for V0.2) |
users |
User accounts with profile and preferences (V0.7) |
user_sessions |
Session tokens with metadata (V0.7) |
login_attempts |
Failed login tracking for rate limiting (V0.7) |
security_events |
Audit log for authentication actions (V0.7) |
docs/{doc_id}/current.md ← always points to latest version
docs/{doc_id}/versions/000001.md ← immutable version snapshots
docs/{doc_id}/versions/000002.md
...
V0.3 adds SQLite FTS5-powered full-text search across all document content.
- Full-text search — titles, headings, summaries, original paths, and cleaned body text are all searchable
- Highlight snippets — search results include
<mark>-tagged excerpts from the best-matching passage - Advanced filters — tag, status, source type, import job, favorites, date range
- Paginated results —
limit/offsetwith a maximum of 100 per page - Search history — recent queries stored and displayed; clearable
- Background indexer — a goroutine polls
document_index_statusevery N seconds and indexes pending documents - Inline indexing — non-import document saves are indexed synchronously (no delay)
- Index status page — shows totals by state (indexed / pending / failed / stale)
- Reindex operations — trigger reindexing for all, failed, stale, or a single document
GET /api/v1/search?q=clickhouse+async_insert&tag=<tag_id>&limit=20&offset=0
Response:
{
"data": {
"items": [
{
"id": "01...",
"title": "ClickHouse 高频写入优化",
"summary": "分析 async_insert、parts、merge...",
"highlights": ["... <mark>async_insert</mark>=1 可以减少客户端小批量写入 ..."],
"score": -1.23,
"tags": ["ClickHouse", "数据库"],
"original_path": "/notes/clickhouse/write.md",
"updated_at": "2026-05-29T12:00:00Z"
}
],
"total": 42,
"limit": 20,
"offset": 0
}
}Query parameters:
| Parameter | Description |
|---|---|
q |
Full-text search query (space-separated terms = implicit AND) |
tag |
Filter by tag ID |
status |
active (default) / archived / all |
source_type |
manual / imported |
review_status |
pending / reviewed |
import_job_id |
Filter by import job |
is_favorite |
1 / 0 |
from / to |
ISO datetime bounds on updated_at |
sort |
relevance (default) / updated_at / title |
order |
asc / desc |
GET /api/v1/search/index-status # aggregate stats
POST /api/v1/search/reindex # trigger rebuild: {"scope":"all|failed|stale|document","document_id":"..."}
GET /api/v1/search/history # recent searches
DELETE /api/v1/search/history # clear all history
From the UI — click the Index tab → Reindex All.
Via API:
curl -X POST http://localhost:8080/api/v1/search/reindex \
-H 'Content-Type: application/json' \
-d '{"scope":"all"}'The background indexer will process all documents within SEARCH_INDEX_INTERVAL_SECONDS (default 5 s).
| State | Meaning |
|---|---|
indexed |
Content is current in the FTS index |
pending |
Awaiting background indexer |
stale |
Document was updated after last indexing |
failed |
Indexer encountered an error (stored in error_message) |
All configuration uses environment variables (or .env file):
| Variable | Default | Description |
|---|---|---|
SEARCH_ENABLED |
true |
Enable FTS search |
SEARCH_INDEX_ON_SAVE |
true |
Index document immediately on create/update |
SEARCH_INDEX_ON_IMPORT |
background |
inline / background / disabled |
SEARCH_INDEX_BATCH_SIZE |
100 |
Documents processed per indexer tick |
SEARCH_INDEX_INTERVAL_SECONDS |
5 |
Background indexer polling interval |
SEARCH_MAX_CONTENT_SIZE_MB |
5 |
Files larger than this are indexed by title/headings only |
SEARCH_SNIPPET_TOKENS |
20 |
Tokens in each highlight snippet |
SEARCH_HISTORY_LIMIT |
100 |
Maximum search history entries retained |
Clicking Open on any search result switches the UI to the Vault tab, loads the document, and highlights every occurrence of the search query inside the CodeMirror editor:
- The Search page calls
onOpenDocument(id, query)when the user clicks Open. App.tsxstores{ id, query }inopenDocstate and switchespageto"vault".VaultPagereceivesinitialDocIdandhighlightQueryas props.- A
useEffectloads the document and stores the query inactiveQuerystate. MarkdownEditorreceiveshighlightQuery={activeQuery}and applies aViewPluginthat scans the document text and wraps every case-insensitive match with acm-search-highlightdecoration (yellow background,#fef08a).- A second
useEffectinside the editor forces a decoration redraw and callsEditorView.scrollIntoViewon the position of the first match, centering it in the viewport.
The highlight is non-destructive — it never modifies document content and disappears when highlightQuery is cleared (e.g. when the user navigates away or opens a different document from the sidebar).
- Uses SQLite FTS5 with
unicode61tokenizer — Chinese word segmentation is character-level, not word-level; search for individual characters or short phrases works, multi-word Chinese phrase search may have lower recall - No semantic / vector search
- No AI question-answering
- No similar-document recommendations
- For very large corpora (100k+ documents) consider tuning
SEARCH_INDEX_BATCH_SIZEandSEARCH_INDEX_INTERVAL_SECONDSbased on available I/O
| Table | Purpose |
|---|---|
documents |
Document metadata, current version pointer |
document_versions |
Version history (storage key + hash per version) |
tags |
Tag definitions |
document_tags |
Document↔tag associations |
import_jobs |
Batch import job tracking |
import_job_items |
Per-file import progress |
document_metadata |
Extracted metadata (headings JSON, code languages) |
document_fts |
SQLite FTS5 virtual table for full-text search |
document_index_status |
Per-document indexing state and hash |
search_history |
Recent search queries |
V0.4 adds rule-based document organization: duplicate detection, similarity analysis, tag suggestions, topic clustering, quality scoring, prompt candidate identification, and a collection system.
No AI, no vector database, no external search engine — all features use SQLite + lightweight heuristics.
- Exact duplicate detection — groups documents sharing the same content hash; user chooses which to keep and what to do with the rest (archive / mark duplicate / ignore)
- Near-duplicate & similarity detection — Jaccard similarity on text shingles within buckets (same tag, import job, directory, title prefix); configurable thresholds
- Similar documents sidebar — per-document similar doc list; ignore or confirm each pair
- Collections — named groups of document references; add, remove, reorder, note; create from search results or topic conversion
- Tag suggestions — rule-based candidates extracted from path, title, headings; user confirms each suggestion
- Topic clustering — rule-based topics from existing tags and path directories; convert any topic to a collection
- Review queue — unified inbox for duplicates, similar pairs, tag suggestions, prompt candidates, low-quality docs; bulk Run All
- Prompt candidate detection — keyword heuristics to flag documents that look like LLM prompts
- Quality scoring — rule-based score (0–100) based on title length, word count, headings, code blocks, favorite status, duplicate status
# Via UI: Duplicates tab → Detect Duplicates
# Via API:
curl -X POST http://localhost:8080/api/v1/organize/detect-duplicates
curl http://localhost:8080/api/v1/organize/duplicates
# Resolve: keep one, archive the rest
curl -X POST http://localhost:8080/api/v1/organize/duplicates/resolve \
-H 'Content-Type: application/json' \
-d '{"keep_document_id":"01...","duplicate_document_ids":["01..."],"action":"archive"}'action values: archive | mark_duplicate | ignore
curl -X POST http://localhost:8080/api/v1/organize/detect-similarity
curl http://localhost:8080/api/v1/documents/{id}/similar
curl -X POST http://localhost:8080/api/v1/organize/similarity/{id}/ignore
curl -X POST http://localhost:8080/api/v1/organize/similarity/{id}/confirmGET /api/v1/collections
POST /api/v1/collections
GET /api/v1/collections/:id
PUT /api/v1/collections/:id
DELETE /api/v1/collections/:id
POST /api/v1/collections/:id/documents
DELETE /api/v1/collections/:id/documents/:document_id
PUT /api/v1/collections/:id/documents/reorder
POST /api/v1/collections/from-search
POST /api/v1/organize/suggest-tags
GET /api/v1/documents/:id/tag-suggestions
POST /api/v1/tag-suggestions/:id/accept
POST /api/v1/tag-suggestions/:id/reject
POST /api/v1/tag-suggestions/batch/accept
POST /api/v1/organize/build-topics
GET /api/v1/topics
GET /api/v1/topics/:id
To convert a topic to a collection, call POST /api/v1/collections/from-search with the topic's document IDs.
GET /api/v1/review/queue?type=duplicates|similar|tag_suggestions|prompt_candidates|low_quality
| Variable | Default | Description |
|---|---|---|
ORGANIZE_DUPLICATE_DETECTION |
true |
Enable exact duplicate detection |
ORGANIZE_SIMILARITY_DETECTION |
true |
Enable near-duplicate detection |
ORGANIZE_TAG_SUGGESTION |
true |
Enable tag suggestions |
ORGANIZE_TOPIC_BUILD |
true |
Enable topic building |
ORGANIZE_NEAR_DUPLICATE_THRESHOLD |
0.85 |
Jaccard threshold for near-duplicate |
ORGANIZE_RELATED_THRESHOLD |
0.70 |
Jaccard threshold for related |
ORGANIZE_MAX_COMPARE_PER_BUCKET |
1000 |
Max pairs compared per bucket |
ORGANIZE_AUTO_ARCHIVE_DUPLICATES |
false |
Never auto-archives without user confirmation |
ORGANIZE_AUTO_APPLY_TAG_SUGGESTIONS |
false |
Never auto-applies tags without confirmation |
ORGANIZE_PROMPT_CANDIDATE_DETECTION |
true |
Enable prompt candidate identification |
| Table | Purpose |
|---|---|
document_similarity |
Detected similarity pairs (exact / near / related) |
collections |
Named document collections |
collection_documents |
Collection membership with sort order |
document_sources |
Document provenance links |
tag_suggestions |
Pending / accepted / rejected tag proposals |
topics |
Rule-derived topic clusters |
topic_documents |
Topic membership with score |
organize_jobs |
Long-running organize operation history |
document_fingerprints |
Lightweight text fingerprints for similarity |
New columns: documents.quality_score, document_metadata.is_prompt_candidate, document_metadata.prompt_score
- V0.4 does not use AI, large language models, or vector databases
- Near-duplicate detection uses Jaccard similarity on text shingles — may produce false positives and false negatives; tune thresholds via config
- Similarity is only computed within buckets (same tag/import job/directory/title prefix) to avoid O(N²) comparisons
- System never automatically deletes documents
- Tag suggestions and similarity resolutions always require explicit user confirmation
- For very large corpora (50k+ documents) tune
ORGANIZE_MAX_COMPARE_PER_BUCKETandORGANIZE_SIMILARITY_BATCH_SIZE
V0.5 adds an AI task queue, per-document summaries, document-grounded Q&A, and prompt extraction. All AI operations are opt-in and traceable to their source documents.
- No whole-library chat — Q&A answers are grounded only in explicitly selected documents
- Documents not sent by default — the AI receives only documents the user explicitly selects
- No content in logs — full Markdown content is never written to server logs
- No hardcoded API keys — keys come from
AI_API_KEYenv var only - AI off by default —
AI_ENABLED=falseuntil explicitly turned on - Source tracking — every AI-generated document records its source document IDs in
document_sources - Saveable output — all AI outputs are saved as Markdown documents in the vault
- Document Summary — enqueue from the Vault tab; result saved as a new Markdown document with source link
- Q&A — ask a question grounded in selected documents; answer with citations; result saved as Markdown
- Prompt Extraction — extract a reusable LLM prompt from any document; saved to the Prompt Library
- Prompt Library — browse, copy, filter by scenario, delete extracted prompts
- AI Task Queue — track pending/running/completed/failed tasks; cancel pending tasks
| Variable | Default | Description |
|---|---|---|
AI_ENABLED |
false |
Enable AI features (must be explicitly set to true) |
AI_PROVIDER |
local_mock |
local_mock | openai_compatible |
AI_BASE_URL |
— | OpenAI-compatible endpoint (e.g. https://api.openai.com/v1) |
AI_API_KEY |
— | API key for the provider |
AI_MODEL |
gpt-4o-mini |
Model name to use |
AI_MAX_CONTEXT_TOKENS |
8000 |
Max tokens to include in context window |
AI_MAX_RETRIES |
2 |
Max task retries before marking failed |
AI_TASK_WORKERS |
2 |
Number of background worker goroutines |
AI_SUMMARY_ENABLED |
true |
Enable document summary tasks |
AI_QA_ENABLED |
true |
Enable Q&A tasks |
AI_PROMPT_EXTRACT_ENABLED |
true |
Enable prompt extraction tasks |
POST /api/v1/ai/tasks/summary # enqueue summary: {"document_id":"..."}
POST /api/v1/ai/tasks/qa # enqueue Q&A: {"question":"...","document_ids":["..."]}
POST /api/v1/ai/tasks/prompt-extract # enqueue prompt extraction: {"document_id":"..."}
GET /api/v1/ai/tasks # list tasks (?status=pending|running|completed|failed)
GET /api/v1/ai/tasks/:id # get task
POST /api/v1/ai/tasks/:id/cancel # cancel pending task
GET /api/v1/ai/documents/:id/summary # get AI summary for a document
GET /api/v1/ai/prompts # list prompt library (?scenario=...)
GET /api/v1/ai/prompts/:id # get prompt
DELETE /api/v1/ai/prompts/:id # delete prompt
| Table | Purpose |
|---|---|
ai_tasks |
AI task queue (pending → running → completed/failed/cancelled) |
document_ai_summaries |
Structured per-document AI summaries |
prompts |
Prompt library (extracted or manually created) |
document_chunks |
Heading-split chunks for context assembly |
- No vector search or semantic similarity — context is assembled by selecting documents explicitly
- No streaming responses — tasks are asynchronous; poll task status or refresh the AI Tasks page
- No whole-library chat — by design; always select specific documents
local_mockprovider returns deterministic stub responses; switch toopenai_compatiblefor real AI
V0.6 focuses on production hardening: system health monitoring, consistency checks, comprehensive testing, and performance benchmarking.
-
GET
/api/v1/system/health— Overall system health status- Database connectivity
- Storage availability
- Search index status
- AI provider status
-
GET
/api/v1/system/stats— Aggregate statistics- Document, collection, tag counts
- Storage usage
- AI task queue depth
- Import job statistics
-
POST
/api/v1/system/doctor— Consistency checks- Storage object integrity (missing files)
- Document version consistency
- Content hash verification
- FTS index coverage
- Tag reference integrity
- Collection reference integrity
- Source document integrity
- Import job consistency
- AI task consistency
-
Go tests — 7 test suites covering core functionality
internal/database/migrate_test.go— Migration idempotency and table creationinternal/storage/local_test.go— Local storage operationsinternal/document/service_test.go— Document CRUD and versioninginternal/search/index_test.go— FTS indexing and searchinternal/organize/duplicate_test.go— Duplicate detectioninternal/ai/task_test.go— AI task queue and processinginternal/system/service_test.go— System health and doctor checks
-
Test fixtures — 11 Markdown files in
e2e/testdata/markdown/- Simple, complex, frontmatter, code blocks
- Links, images, tables
- Duplicate pairs for testing
- Similar documents for testing
- Large documents for performance testing
-
E2E tests — Playwright smoke tests
- Navigation to all pages
- API endpoint verification
- Health, stats, doctor endpoints
cmd/bench/main.go— Benchmark tool- Configurable document count
- Search query performance
- Indexing throughput
- JSON output for automation
# Run benchmark with 1000 documents and 20 search queries
go run ./cmd/bench --docs 1000 --queries 20
# JSON output for CI
go run ./cmd/bench --docs 500 --queries 10 --jsonmake test # Run all tests
make test-go # Run Go tests
make test-web # Run frontend type-check and build
make test-e2e # Run Playwright E2E tests (requires: cd e2e && pnpm install && pnpm exec playwright install)
make doctor # Run doctor check against running server
make bench # Run benchmark (1000 docs, 20 queries)Check system health:
curl http://localhost:8080/api/v1/system/healthResponse:
{
"status": "ok",
"database": "ok",
"storage": "ok",
"search": "ok",
"ai": "disabled"
}Get aggregate statistics:
curl http://localhost:8080/api/v1/system/statsResponse:
{
"documents": 150,
"versions": 420,
"collections": 12,
"tags": 45,
"import_jobs": 3,
"indexed_documents": 150,
"pending_indexes": 0,
"failed_indexes": 0,
"ai_tasks": 5,
"prompts": 8
}Run consistency checks:
# Run all checks
curl -X POST http://localhost:8080/api/v1/system/doctor \
-H 'Content-Type: application/json' \
-d '{"checks":[]}'
# Run specific checks
curl -X POST http://localhost:8080/api/v1/system/doctor \
-H 'Content-Type: application/json' \
-d '{"checks":["storage_objects","document_versions","fts_index"]}'Response:
{
"status": "ok",
"checks": [
{
"name": "storage_objects",
"status": "ok",
"total": 150,
"failed": 0,
"items": []
},
{
"name": "document_versions",
"status": "ok",
"total": 150,
"failed": 0,
"items": []
}
]
}# Run all Go tests
go test ./...
# Run specific package tests
go test ./internal/document
go test ./internal/search
go test ./internal/system
# Run with coverage
go test -cover ./...
# Run E2E tests
cd e2e
pnpm install
pnpm exec playwright install
pnpm testExample benchmark output:
Cloud Vault Benchmark Results
============================
Documents created: 1000
Create duration: 2.345s (2.35 ms/doc)
Search queries: 20
Total search duration: 156ms
Average search time: 7.8ms
Indexed documents: 1000
Index coverage: 100.0%
JSON output:
{
"doc_count": 1000,
"create_duration_ms": 2345,
"search_queries": 20,
"search_duration_ms": 156,
"avg_search_ms": 7,
"indexed_docs": 1000
}- E2E tests require manual Playwright installation
- Benchmark uses local storage only (no cloud storage benchmarking)
- Doctor checks are read-only (no auto-repair)
- No scheduled doctor runs (must be triggered manually)
- No metrics export (Prometheus/Graphite)
V0.7 adds foundational productization features: session-based authentication, internationalization (3 locales), theme system (light/dark/system), and account management.
- Session-based authentication — secure cookie-based sessions with automatic renewal
- Admin setup flow — first-time setup wizard to create initial admin account
- Login rate limiting — configurable failed login attempt limits with automatic lockout
- Password management — secure password change with current password verification
- Account settings — update display name, email, locale, and theme preferences
- Security page — view active sessions, revoke sessions, view security events
- Theme system — light/dark/system theme with persistent preferences
- i18n support — English, 简体中文, 繁體中文 with automatic language detection
- Route protection — all API endpoints protected except public auth routes
- Security events — audit trail for login, logout, password changes, session revocation
- Doctor auth checks — system health checks for auth-related issues
When you first access the application, you'll see a setup page to create your admin account:
- Visit
http://localhost:8080 - Fill in username, email, and password (minimum 10 characters)
- Click "Create Admin Account"
- You'll be automatically logged in and redirected to the main application
Auth settings in config.toml:
[auth]
enabled = true
session_ttl_hours = 168 # 7 days
cookie_name = "markdown_vault_session"
secure_cookie = false # Set to true in production with HTTPS
max_login_failures = 5
login_failure_window_minutes = 15
lockout_minutes = 30
password_min_length = 10
bootstrap_admin_enabled = false # Set to true for automated setup
[auth.bootstrap_admin]
username = "admin"
email = "admin@vault.birdor.com"
password = "Change-Me-Strong-Password-123"All auth settings can be overridden via environment variables:
AUTH_ENABLED=true
AUTH_SESSION_TTL_HOURS=168
AUTH_SECURE_COOKIE=true
AUTH_MAX_LOGIN_FAILURES=5
AUTH_LOGIN_FAILURE_WINDOW_MINUTES=15
AUTH_LOCKOUT_MINUTES=30
AUTH_PASSWORD_MIN_LENGTH=10
AUTH_COOKIE_NAME=markdown_vault_sessionBootstrap admin settings:
AUTH_BOOTSTRAP_ADMIN_ENABLED=true
AUTH_BOOTSTRAP_ADMIN_USERNAME=admin
AUTH_BOOTSTRAP_ADMIN_EMAIL=admin@vault.birdor.com
AUTH_BOOTSTRAP_ADMIN_PASSWORD=Change-Me-Strong-Password-123All auth endpoints are under /api/v1/auth.
POST /api/v1/auth/setupCreate initial admin account (only works when no users exist).
Request:
{
"username": "admin",
"email": "admin@vault.birdor.com",
"password": "StrongPassword123"
}GET /api/v1/auth/statusCheck if system is initialized.
Response:
{
"data": {
"initialized": true
}
}POST /api/v1/auth/loginLogin with username/email and password.
Request:
{
"username": "admin",
"password": "StrongPassword123"
}Response sets Set-Cookie header with session token.
GET /api/v1/healthSystem health check (always public).
POST /api/v1/auth/logoutLogout and revoke current session.
GET /api/v1/auth/meGet current user profile.
PUT /api/v1/auth/meUpdate user profile.
Request:
{
"display_name": "Admin User",
"email": "admin@vault.birdor.com",
"locale": "en-US",
"theme": "dark"
}POST /api/v1/auth/change-passwordChange password (requires current password).
Request:
{
"current_password": "OldPassword123",
"new_password": "NewStrongPassword456"
}GET /api/v1/auth/sessionsList all active sessions for current user.
Response:
{
"data": {
"sessions": [
{
"id": "01K...",
"user_agent": "Mozilla/5.0...",
"ip_address": "192.168.1.100",
"created_at": "2026-05-29T10:00:00Z",
"expires_at": "2026-06-05T10:00:00Z"
}
]
}
}POST /api/v1/auth/sessions/revoke-allRevoke all sessions except current.
All other API endpoints (/api/v1/documents, /api/v1/search, etc.) now require authentication and return 401 Unauthorized if not logged in.
- Minimum 10 characters (configurable via
password_min_length) - Must contain at least one uppercase letter
- Must contain at least one lowercase letter
- Must contain at least one digit
- Passwords are hashed using PBKDF2-SHA512 (210,000 iterations)
- Session tokens stored in HttpOnly cookies (not accessible via JavaScript)
- Session tokens hashed using SHA-256 before storage
- Configurable session TTL (default 7 days)
- Secure cookie flag for HTTPS deployments
- SameSite=Lax to prevent CSRF
- Failed login attempts tracked per username
- After 5 failed attempts within 15 minutes, account locked for 30 minutes
- Rate limit counter resets after successful login
- All rate limit violations logged as security events
All authentication actions are logged to security_events table:
login_success/login_failedlogoutpassword_changedsession_revokedaccount_lockedadmin_setup/admin_bootstrap
View security events in the Security page or via API:
GET /api/v1/auth/security-events?limit=50&offset=0Three theme modes available:
- Light — light background with dark text
- Dark — dark background with light text
- System — follow OS preference (via
prefers-color-scheme)
Theme preference stored in user profile and synced to localStorage for instant application on page load.
Change theme via:
- Account settings page → Theme dropdown
- API:
PUT /api/v1/auth/mewith{"theme": "dark"}
Supported locales:
en-US— English (US)zh-CN— 简体中文 (Simplified Chinese)zh-TW— 繁體中文 (Traditional Chinese)
Locale preference stored in user profile and synced to localStorage.
Change locale via:
- Account settings page → Language dropdown
- API:
PUT /api/v1/auth/mewith{"locale": "zh-CN"}
i18n covers:
- Navigation tabs (Vault, Search, Import, etc.)
- Common actions (Save, Cancel, Delete, etc.)
- Auth pages (Login, Setup, Account, Security)
- Form labels and error messages
- Theme names (Light, Dark, System)
- Locale names (English, 简体中文, 繁體中文)
- Shown on first visit when no users exist
- Creates initial admin account
- Auto-login after successful setup
- Username/email + password form
- Error messages for invalid credentials
- Rate limit warnings when account locked
- Update display name, email
- Change locale (language)
- Change theme (light/dark/system)
- Save button to persist changes
- Change password (requires current password)
- View active sessions with device info
- Revoke individual sessions
- Revoke all other sessions button
- View security event audit log
New tables in migration 006:
| Table | Purpose |
|---|---|
users |
User accounts with profile preferences |
user_sessions |
Active session tokens with metadata |
login_attempts |
Failed login tracking for rate limiting |
security_events |
Audit log for authentication actions |
Run auth-related health checks:
curl -X POST http://localhost:8080/api/v1/system/doctor \
-H 'Content-Type: application/json' \
-d '{"checks":["auth"]}'Checks:
users_table_exists— verify users table existshas_admin_user— at least one admin user existsexpired_sessions— count of expired but not revoked sessionsorphaned_sessions— sessions referencing non-existent usersorphaned_security_events— security events referencing non-existent users
-
Enable HTTPS in production
- Set
secure_cookie = truein config - Configure reverse proxy (nginx/caddy) with SSL certificate
- Redirect all HTTP traffic to HTTPS
- Set
-
Use strong passwords
- Minimum 10 characters enforced
- Consider requiring special characters for admin accounts
-
Regular password rotation
- Change admin password periodically
- All sessions automatically revoked after password change
-
Monitor security events
- Check Security page regularly
- Look for unusual login patterns or locations
- Revoke suspicious sessions immediately
-
Disable bootstrap after setup
- Set
bootstrap_admin_enabled = falseafter initial setup - Prevents accidental admin account creation
- Set
-
Backup database regularly
userstable contains hashed passwordsuser_sessionscontains active session tokenssecurity_eventscontains audit trail
- Single-user or small-scale private deployment only
- No OAuth/SSO integration
- No role-based access control (RBAC)
- No multi-tenancy
- No email verification
- No password reset via email
- No two-factor authentication (2FA)
- Rate limiting is per-username only (no IP-based limiting)
- Session cleanup requires manual triggering or scheduled job
- i18n covers core UI but not all error messages
- Bootstrap admin requires server restart after config changes
If upgrading from V0.6:
- Backup your database:
cp data/app.db data/app.db.backup - Stop the running application
- Run migration:
go run ./cmd/server(migration 006 runs automatically) - Access the application — you'll see the setup page
- Create your admin account
- All existing documents and data remain intact
If you want to skip the setup page, enable bootstrap admin in config.toml before starting:
[auth]
enabled = true
bootstrap_admin_enabled = true
[auth.bootstrap_admin]
username = "admin"
email = "admin@vault.birdor.com"
password = "Change-Me-Strong-Password-123"The bootstrap admin will be created automatically on startup, and you can login immediately.
V0.8 focuses on production readiness: configuration validation, backup/restore, deployment packaging, deployment-aware doctor checks, and comprehensive regression testing.
Configuration is loaded in this order (later wins):
- Defaults — built-in defaults
- config.toml — TOML file (default:
./config.toml, or--config <path>) - .env file — KEY=VALUE pairs (default:
./.env) - Environment variables —
APP_*,AUTH_*,QINIU_*, etc. - CLI flags —
--addr,--db-path, etc.
# Copy example configs
cp config.example.toml config.toml
cp env.example .env
# Edit .env with your secrets
nano .env
# Run the server
./cloud-vault --config config.tomlOn startup, Cloud Vault validates all configuration and refuses to start with invalid settings:
| Section | Required Fields |
|---|---|
[server] |
addr non-empty, port 1–65535 |
[database] |
path non-empty, parent dir writable |
[storage.local] |
root non-empty, writable |
[storage.qiniu] |
access_key, secret_key, bucket, domain, region all non-empty |
[auth] (enabled) |
cookie_name, session_ttl_hours > 0, password_min_length >= 8 |
[ai] (enabled + openai) |
base_url, api_key, model non-empty |
Validation errors are structured with field paths — secrets are never logged.
All log output automatically masks sensitive fields:
Qiniu.SecretKey→[REDACTED]AI.APIKey→[REDACTED]Auth.BootstrapAdmin.Password→[REDACTED]
Via UI: System tab → Backup Management → Create Backup
Via API:
curl -X POST http://localhost:8080/api/v1/system/backup \
-H "Cookie: session=<your-session-cookie>"Via CLI:
make backupBackups are stored as backups/cloud-vault-backup-YYYYMMDD-HHMMSS.zip containing:
- SQLite database (consistent snapshot via
VACUUM INTO) - Local storage objects (if provider=local)
manifest.jsonwith metadata
# List all backups
curl http://localhost:8080/api/v1/system/backups \
-H "Cookie: session=<your-session-cookie>"
# Download a specific backup
curl -O http://localhost:8080/api/v1/system/backups/cloud-vault-backup-20260530-120000.zip/download \
-H "Cookie: session=<your-session-cookie>"
# Delete a backup
curl -X DELETE http://localhost:8080/api/v1/system/backups/cloud-vault-backup-20260530-120000.zip \
-H "Cookie: session=<your-session-cookie>"Via CLI (recommended):
# Stop the server first!
./cloud-vault restore --file backups/cloud-vault-backup-20260530-120000.zip --data-dir ./dataSteps:
- Validates the backup zip exists and contains a valid manifest
- Extracts
app.dbto<data-dir>/app.db.new - Extracts
objects/to<data-dir>/objects.new/ - Atomically renames to replace existing files
- Prints "run doctor to verify"
Via API (requires confirmation):
curl -X POST http://localhost:8080/api/v1/system/restore \
-H "Content-Type: application/json" \
-H "Cookie: session=<your-session-cookie>" \
-d '{"backup_name":"cloud-vault-backup-20260530-120000.zip","confirm":"RESTORE"}'Use the offline CLI restore path for production data. The API restore endpoint requires explicit confirm=RESTORE; do not run it while users or background writers are active.
Note: Qiniu mode backups include only the manifest — bucket contents must be restored using Qiniu's own backup tools.
V0.8 adds 8 deployment-aware checks to the doctor system:
| Check | What it verifies |
|---|---|
config_check |
All config sections pass ValidateConfig() |
data_dir_check |
Data directory exists and is writable |
storage_writable_check |
Can write/read/delete a test object |
auth_security_check |
Auth enabled, admin exists, no disabled users with sessions |
cookie_security_check |
secure_cookie=true (warns if false) |
qiniu_config_check |
All Qiniu fields set, bucket accessible |
backup_check |
Backups dir writable, last backup < 7 days |
migration_check |
Schema version matches expected latest |
Run deployment checks:
curl -X POST http://localhost:8080/api/v1/system/doctor \
-H "Content-Type: application/json" \
-H "Cookie: session=<your-session-cookie>" \
-d '{"checks":["config_check","data_dir_check","storage_writable_check","auth_security_check","cookie_security_check","backup_check","migration_check"]}'V0.8 includes comprehensive migration upgrade tests covering:
empty → latest(fresh install)v01 → latest(documents + versions only)v03 → latest(+ FTS, search history)v05 → latest(+ AI tasks, prompts, sources)v07 → latest(+ users, sessions, security events)latest → latest(idempotency)
All migrations are idempotent — running db.Migrate() multiple times is safe.
Build a production release package:
make release-v1 VERSION=1.0.0
# or
./scripts/release-v1.sh 1.0.0Produces dist/server/markdown-vault and release metadata under dist/ containing:
markdown-vaultserver binary (linux/amd64 by default)config.example.toml.env.exampleREADME.md
Build the image:
docker build -t cloud-vault:latest .Run with docker-compose:
cp docker-compose.yml docker-compose.override.yml
# Edit to set volumes, ports, environment
docker-compose up -dRun standalone:
docker run -d \
--name cloud-vault \
-p 8080:8080 \
-v ./data:/app/data \
-v ./backups:/app/backups \
-e AUTH_ENABLED=true \
-e AUTH_SECURE_COOKIE=true \
cloud-vault:latestThe Docker image uses a multi-stage build:
- frontend-builder — Node 20 Alpine, builds React app
- backend-builder — Go 1.26 Alpine, compiles Go binary
- runtime — Alpine Linux, minimal footprint
Install as a systemd service on Linux:
# Build release binary
make release
# Create system user
sudo useradd --system --shell /sbin/nologin cloud-vault
# Install
sudo mkdir -p /opt/cloud-vault
sudo cp dist/cloud-vault/cloud-vault /opt/cloud-vault/
sudo cp config.example.toml /opt/cloud-vault/config.toml
sudo mkdir -p /opt/cloud-vault/data /opt/cloud-vault/backups
sudo chown -R cloud-vault:cloud-vault /opt/cloud-vault
# Edit config
sudo nano /opt/cloud-vault/config.toml
# Install service
sudo cp deploy/systemd/cloud-vault.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now cloud-vaultThe systemd unit includes security hardening:
ProtectSystem=strict— prevents writes outside designated pathsPrivateTmp=true— isolates temporary filesNoNewPrivileges=true— prevents privilege escalationReadWritePaths=/opt/cloud-vault/data /opt/cloud-vault/backups— explicit write paths
See deploy/systemd/INSTALL.md for full documentation.
The Settings page (Settings tab) displays non-sensitive configuration:
- App version
- Storage provider (Local / Qiniu)
- Auth enabled status
- Search enabled status
- AI enabled status
- Database path
- Storage root (local mode only)
Secrets (API keys, passwords) are never shown.
-
Production: enable authentication
[auth] enabled = true secure_cookie = true
-
Use HTTPS
- Terminate TLS at a reverse proxy (nginx, Caddy, Traefik)
- Set
AUTH_SECURE_COOKIE=true - Redirect all HTTP to HTTPS
-
Never commit secrets to Git
- Use
.envfile (add to.gitignore) - Inject Qiniu/AI keys via environment variables
- Use a secrets manager in production (Vault, AWS Secrets Manager)
- Use
-
Never log secrets
- Cloud Vault automatically masks sensitive fields in logs
- Do not override this behavior
-
Back up regularly
- SQLite:
cloud-vault backupor API - Local storage: included in backup zip
- Qiniu: use Qiniu's own backup tools
- SQLite:
-
Rotate passwords periodically
- Change admin password via Security page
- All sessions automatically revoked after password change
-
Monitor security events
- Check Security page for unusual login patterns
- Revoke suspicious sessions immediately
- Restore recommended offline — concurrent writes during restore are undefined
- Qiniu mode doesn't auto-backup bucket contents — use Qiniu's own backup tools
- Docker/systemd are examples — production hardening (secrets management, health checks, TLS termination) is user responsibility
- No incremental backups — every backup is a full snapshot
- No scheduled backup — use external cron/systemd timer
- No backup encryption at rest — zip is stored plain; encrypt if storing offsite
- No automatic rotation of old backups — manual cleanup required
- Multi-user permissions and team collaboration not implemented
If upgrading from V0.7:
- Backup your database:
cp data/app.db data/app.db.backup - Stop the running application
- Run migration:
./cloud-vault(migration 008 runs automatically) - Access the application — no setup required, all users and sessions preserved
- Review new deployment checks: System tab → Doctor → select deployment checks
All existing documents, users, sessions, and data remain intact.
V0.9 wraps Cloud Vault in a Wails v2 desktop application with system tray, native directory picker, and desktop-optimized import experience.
cmd/desktop/main.go → Wails entry point
internal/desktop/
├── config.go → DesktopConfig + platform data dirs
├── server.go → Local HTTP server (127.0.0.1:0 + token)
├── service.go → DesktopService: runtime info, scan preview
├── bindings.go → WailsBindings: methods exposed to frontend
├── tray.go → TrayManager: systray icon + menu
└── data_dir.go → Open data directory in file manager
internal/app/app.go → HTTPHandler() + StartBackgroundTasks()
The desktop app:
- Starts a local HTTP server on
127.0.0.1:0(never0.0.0.0) - Generates a random 32-byte token for authentication
- Embeds the existing React frontend via Wails AssetServer
- Reuses all existing backend routes and services unchanged
- Provides desktop-specific APIs via Wails bindings
Desktop mode stores all data in the system App Data directory:
| Platform | Default Path |
|---|---|
| macOS | ~/Library/Application Support/CloudVault |
| Windows | %APPDATA%\CloudVault |
| Linux | $XDG_DATA_HOME/cloudvault or ~/.local/share/cloudvault |
The directory contains:
CloudVault/
├── app.db ← SQLite database
├── objects/ ← Local object storage
├── backups/ ← Backup archives
├── logs/ ← Application logs
└── cache/ ← Temporary cache
Desktop-specific config in config.toml:
[desktop]
app_name = "Cloud Vault"
data_dir = "" # auto-detect by platform
close_to_tray = true
native_notifications = true
launch_at_login = falseEnvironment variables:
| Variable | Description |
|---|---|
DESKTOP_APP_NAME |
Application name shown in tray |
DESKTOP_DATA_DIR |
Override data directory path |
DESKTOP_CLOSE_TO_TRAY |
Close to tray instead of quit (true/false) |
DESKTOP_NATIVE_NOTIFICATIONS |
Enable native desktop notifications |
DESKTOP_LAUNCH_AT_LOGIN |
Start at login (not yet implemented) |
The tray icon provides quick access to common actions:
| Menu Item | Action |
|---|---|
| Show Window | Restore the main window |
| Open Data Directory | Open data folder in file manager |
| Scan Import Directory | Preview files in import directory |
| Runtime Info | Display runtime information |
| Close to Tray | Toggle close-to-tray behavior |
| Quit | Exit the application |
When close-to-tray is enabled, closing the window hides it instead of quitting.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/system/runtime |
Runtime info (mode, port, data dir, platform) |
| POST | /api/v1/import-jobs/scan-preview |
Preview scan of import directory |
| GET | /api/v1/desktop/data-dir |
Data directory info |
| POST | /api/v1/desktop/open-data-dir |
Open data directory in file manager |
All endpoints require the token passed via ?token=... query parameter or X-Auth-Token header.
The frontend can call these methods via Wails runtime:
| Method | Description |
|---|---|
GetRuntimeInfo() |
Get runtime information |
SelectDirectory(title, defaultPath) |
Open native directory picker |
ScanPreview(req) |
Preview scan of directory |
OpenDataDirectory() |
Open data folder in file manager |
ShowNotification(title, message) |
Show desktop notification |
StartImport(jobID, req) |
Start import with progress tracking |
GetConfig() |
Get desktop configuration |
UpdateConfig(config) |
Update desktop configuration |
Quit() |
Quit the application |
MinimizeToTray() |
Hide window to tray |
RestoreFromTray() |
Show window from tray |
# Development mode with hot reload
make desktop-dev
# Production build
make desktop-build
# Package for distribution
make desktop-package
# Clean build artifacts
make desktop-clean# Build and run
go build -o bin/cloud-vault-desktop ./cmd/desktop
./bin/cloud-vault-desktop
# Or use go run
go run ./cmd/desktopDesktop mode enforces strict security:
- Local-only binding — HTTP server binds to
127.0.0.1only, never0.0.0.0 - Random port — Port is selected randomly from available ports
- Token authentication — 32-byte random token required for all API calls
- Timing-safe comparison — Token validation uses constant-time comparison
- No secrets in logs — Token and sensitive config never logged
| Feature | Server Mode (cmd/server) |
Desktop Mode (cmd/desktop) |
|---|---|---|
| Entry point | cmd/server/main.go |
cmd/desktop/main.go |
| Bind address | Configurable (APP_ADDR) |
Always 127.0.0.1:0 |
| Authentication | Cookie-based sessions | Token-based (random) |
| Data directory | Configurable (DB_PATH) |
System App Data |
| UI | Browser-based | Wails native window |
| System tray | No | Yes |
| TLS | Configurable | Disabled |
- Launch at login — not yet implemented
- Auto-update — not yet implemented
- Multi-window — single window only
- Offline sync — not yet implemented
- Native notifications on Linux — falls back to logging
Desktop mode uses a separate data directory from server mode. To migrate data:
- Export from server mode:
cp data/app.db ~/Library/Application\ Support/CloudVault/app.db - Copy objects:
cp -r data/objects ~/Library/Application\ Support/CloudVault/objects - Start desktop mode — data is available immediately
See docs/testing/desktop-v0.9-checklist.md for the complete verification checklist covering:
- Build verification
- Configuration validation
- HTTP server security
- Desktop service APIs
- System tray functionality
- Wails bindings
- Platform-specific behavior
- Security requirements
- Performance requirements
- No multi-user collaboration (single admin account only)
- No semantic or vector search
- No OAuth/SSO integration
- No role-based access control (RBAC)
- No multi-tenancy support
- No two-factor authentication (2FA)
- No email verification or password reset
- No incremental or scheduled backups
- No backup encryption at rest