Jesse A. Green / notebook

Memos Administrator & Developer Guide

Generated May 2026 from usememos/memos at bcbcb03

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is the complete operational and architectural reference for administrators, DevOps engineers, and contributors deploying and maintaining Memos v0.26.2 (bcbcb03). For day-to-day usage, note creation, and user-facing features, see the companion User Guide at User Guide.


1. Architecture Overview

Note

Memos is a monolithic binary — a single compiled Go executable that simultaneously serves the REST/gRPC API, the React single-page application, and all static assets. There are no separate frontend or backend processes, no message broker, and no cache daemon. This design makes deployment trivially simple but means all components share one process and one port.

1.1 High-Level Topology

                         ┌───────────────────────────────────────────────┐
                         │              memos binary                      │
                         │                                                │
Internet ──── :8081 ───►│  Echo v5 HTTP Server                          │
  (or Unix sock)         │  ├── /healthz          HealthService           │
                         │  ├── /api/v1/*         gRPC-Gateway → gRPC     │
                         │  ├── /file/attachments  FileServer (HTTP)      │
                         │  ├── /file/users        FileServer (avatars)   │
                         │  ├── /rss/*             RSS feed generator     │
                         │  ├── /mcp               MCP server             │
                         │  └── /*                 SPA (embedded dist/)   │
                         │                                                │
                         │  Background Goroutines                         │
                         │  ├── S3 presign refresher                      │
                         │  └── Memo payload updater                      │
                         │                                                │
                         │  Store Layer                                   │
                         │  ├── In-memory cache                           │
                         │  └── DB driver (SQLite │ MySQL │ PostgreSQL)   │
                         └───────────────────────────────────────────────┘

1.2 Request Lifecycle

  1. An HTTP request arrives at the Echo server.
  2. The Authenticator middleware resolves the caller's identity from the Authorization header (Bearer JWT or PAT) or the memos_refresh cookie. Anonymous requests proceed with no identity.
  3. The request is dispatched to the matching route:
    • /api/v1/* → gRPC-Gateway transcodes JSON↔Protobuf → gRPC service handler.
    • /file/* → Native HTTP file server with range-request and thumbnail support.
    • /mcp → MCP server with CSRF origin validation.
    • /rss/* → RSS generator.
    • Everything else → embedded index.html for SPA client-side routing.
  4. gRPC handlers call the Store (cache-first, then DB driver).
  5. Response returns through Echo (middleware chain is minimal — recovery only; no global rate limiter).

1.3 Component Map

Component Source Path Role
CLI entry cmd/memos/main.go Cobra commands, flag parsing, startup orchestration
Server server/server.go Echo server creation, route registration, graceful shutdown
Auth middleware server/auth/ JWT/PAT validation, refresh token rotation
API V1 server/router/api/v1/ All gRPC service handlers
ACL config server/router/api/v1/acl_config.go Method-level role enforcement
SSE hub server/router/api/v1/sse_hub.go Server-Sent Events for live updates
File server server/router/fileserver/ Attachment/avatar serving, thumbnail generation
Frontend server/router/frontend/ Embedded SPA serving
MCP server server/router/mcp/ Model Context Protocol endpoint
RSS server/router/rss/ RSS feed generation
S3 presign runner server/runner/s3presign/ Background S3 URL refresh
Memo payload runner server/runner/memopayload/ Background memo metadata updates
Store store/ Data access layer (driver interface + implementations)
Store cache store/cache/ In-memory caching wrapper
DB drivers store/db/sqlite/, store/db/mysql/, store/db/postgres/ Database adapters
Migrations store/migration/{sqlite,mysql,postgres}/ Versioned schema migrations
AI client internal/ai/ OpenAI / Google Gemini transcription
Cron internal/cron/ Cron scheduling library
Scheduler internal/scheduler/ Job scheduler with middleware
Email internal/email/ SMTP email client
CEL filter internal/filter/ CEL-based query filter engine
Markdown internal/markdown/ Goldmark processor, tag/mention extensions
Webhook internal/webhook/ Outbound webhook delivery, SSRF protection
HTTP getter internal/httpgetter/ Safe HTML meta extraction and image fetching
OAuth2 IDP internal/idp/ Identity provider integration
Profile internal/profile/ Server configuration profile struct
Version internal/version/ Version parsing and comparison
S3 storage internal/storage/s3/ S3 client wrapper
Protobuf proto/api/v1/ Service and message definitions
Web frontend web/ React 18 SPA (Vite, TypeScript, Tailwind CSS 4)

1.4 Technology Stack

Layer Technology Version
Language Go 1.26.1
HTTP framework Echo v5
API protocol gRPC + gRPC-Gateway + ConnectRPC —
SQLite driver modernc.org/sqlite Pure Go, CGO-free
MySQL support go-sql-driver/mysql —
PostgreSQL support lib/pq —
Frontend framework React 18
Frontend build Vite —
CSS Tailwind CSS v4
UI components Radix UI —
State management TanStack React Query —
Markdown (backend) Goldmark —
Markdown (frontend) react-markdown —
Auth tokens golang-jwt/jwt v5
OAuth2 golang.org/x/oauth2 —
MCP mcp-go (mark3labs) —
AI providers OpenAI API, Google Gemini —
Password hashing bcrypt —

1.5 Startup Sequence

cmd/memos/main.go
  │
  ├─ Parse flags (Cobra + Viper)
  ├─ Build Profile (data dir, addr, port, driver, DSN)
  ├─ Validate Profile (dir writable, driver recognized)
  ├─ Init DB driver (open connection, verify connectivity)
  ├─ Run migrations (store.Migrate())
  ├─ Create Store (driver + cache wrapper)
  ├─ Create Server (server.NewServer())
  │     ├─ Create Echo instance
  │     ├─ Register recovery middleware
  │     ├─ Get/create basic instance setting (secret_key, schema_version)
  │     ├─ Register /healthz
  │     ├─ Register frontend static files (embedded)
  │     ├─ Create gRPC server + register all services
  │     ├─ Create gRPC-Gateway (HTTP/JSON bridge)
  │     ├─ Register file server routes
  │     ├─ Create + register RSS routes
  │     ├─ Register gRPC-Gateway routes (/api/v1/*)
  │     ├─ Register MCP server (/mcp)
  │     └─ Start background runners (S3 presign, memo payload)
  └─ server.Start() → listen + signal handling (SIGTERM/SIGINT)
        └─ Graceful shutdown:
              stop runners → close SSE hub → shutdown HTTP → wait runners → close DB

1.6 gRPC Services

All gRPC services are defined in proto/api/v1/ and exposed over HTTP via gRPC-Gateway at /api/v1/. The web frontend communicates using ConnectRPC (a browser-compatible gRPC transport).

Service Proto file Key operations
MemoService memo_service.proto CRUD memos, list, search, comments, share
UserService user_service.proto CRUD users, settings, stats
AuthService auth_service.proto Sign in, sign up, token refresh
AttachmentService attachment_service.proto CRUD attachments, link to memos
InstanceService instance_service.proto Instance settings, admin operations
IDPService idp_service.proto Identity provider management
ShortcutService shortcut_service.proto Saved search shortcuts
AIService ai_service.proto Audio/image transcription
HealthService health_service.proto Health check, SSE stream

2. System Requirements

2.1 Minimum Requirements

Resource Minimum Recommended
CPU 1 core 2+ cores
RAM 256 MB 512 MB–1 GB
Disk (binary + data) 200 MB + data 1 GB+ (depends on attachments)
OS Linux, macOS, Windows Linux (amd64/arm64)
Go (build only) 1.26.1 —
Node.js (build only) 20+ —

2.2 Database Requirements

Database Version Notes
SQLite Built-in (modernc.org/sqlite) Default; no external server; single-file DB
MySQL 8.0+ External server required; set --driver mysql
PostgreSQL 14+ External server required; set --driver postgres

Note

SQLite uses a pure Go driver (no CGO). The binary runs on any platform without external libraries. For production workloads with multiple concurrent writers, MySQL or PostgreSQL are recommended.

2.3 Network Requirements

Port Protocol Purpose
8081 (default) TCP/HTTP API + frontend + all routes
— Unix socket Optional alternative to TCP

Memos does not open any outbound ports during normal operation. Outbound connections occur only for:

  • S3 storage (user-configured endpoint)
  • OAuth2 identity providers (user-configured)
  • SMTP email (user-configured)
  • Webhook delivery (outbound to configured URLs)
  • AI providers (OpenAI/Gemini endpoints)

2.4 Storage Requirements

Storage type Dependency
Local filesystem Data directory writable by the memos process
S3-compatible Bucket with read/write access; credentials in instance settings
Database blob No filesystem; stored in attachment table

3. Configuration Reference

Important

Memos has two configuration layers: CLI flags (and their environment variable equivalents) set server-level parameters at startup, and instance settings (stored in the database) control runtime behavior and can be changed live through the admin UI without restarting the server. CLI flags cannot be changed without a restart.

3.1 CLI Flags

All flags can be set as environment variables using the MEMOS_ prefix and upper-casing the flag name (e.g., --port → MEMOS_PORT).

Flag Env Variable Default Description
--demo MEMOS_DEMO false Enable demo mode (pre-populated read-only data)
--addr MEMOS_ADDR "" (all interfaces) TCP bind address
--port MEMOS_PORT 8081 TCP port
--unix-sock MEMOS_UNIX_SOCK "" Unix socket path (overrides addr/port when set)
--data MEMOS_DATA Platform-dependent Data directory for DB file, uploads, cache
--driver MEMOS_DRIVER sqlite Database driver: sqlite, mysql, postgres
--dsn MEMOS_DSN "" Database connection string (required for MySQL/PostgreSQL)
--instance-url MEMOS_INSTANCE_URL "" Public base URL (used in RSS, email links, MCP)
--allow-private-webhooks MEMOS_ALLOW_PRIVATE_WEBHOOKS false Allow webhook delivery to private/internal IP addresses

Default data directory by platform:

  • Linux: ~/.memos/
  • macOS: ~/Library/Application Support/memos/
  • Windows: %APPDATA%\memos\

DSN formats:

# MySQL
--dsn "user:password@tcp(host:3306)/memos?parseTime=true"

# PostgreSQL
--dsn "host=localhost port=5432 user=memos password=secret dbname=memos sslmode=disable"

# SQLite (explicit path)
--dsn "/data/memos.db"

3.2 Instance Settings

These settings live in the instance_setting table and are managed through Settings → Admin in the UI or via the InstanceService gRPC API. They take effect immediately without restarting the server.

3.2.1 Basic Settings

Key Type Description
secret_key string JWT signing secret (auto-generated UUID on first run; rotating this invalidates all existing sessions)
schema_version string Current DB schema version (do not edit manually)

3.2.2 General Settings

Key Type Default Description
disallow_user_registration bool false Prevent new user self-registration
disallow_password_auth bool false Disable username/password login (forces OAuth2-only)
additional_script string "" Custom JavaScript injected into every page
additional_style string "" Custom CSS injected into every page
custom_profile.title string "" Instance title shown on login page
custom_profile.description string "" Instance description
custom_profile.logo_url string "" URL to custom logo
week_start_day_offset int 0 Calendar week start (0=Sunday, 1=Monday)
disallow_change_username bool false Prevent users from changing their username
disallow_change_nickname bool false Prevent users from changing their nickname

3.2.3 Storage Settings

Key Type Default Description
storage_type enum DATABASE Where attachments are stored: DATABASE, LOCAL, S3
filepath_template string {{.year}}/{{.month}}/{{.day}}/{{.filename}} Path template for local storage
upload_size_limit_mb int 32 Maximum attachment upload size in MB

S3 configuration (only active when storage_type = S3):

Key Description
s3_config.access_key_id AWS/S3 access key ID
s3_config.access_key_secret AWS/S3 secret access key
s3_config.endpoint S3 endpoint URL (for S3-compatible services like MinIO, Cloudflare R2)
s3_config.region AWS region (e.g., us-east-1)
s3_config.bucket Bucket name
s3_config.use_path_style true for MinIO/path-style; false for AWS virtual-hosted style

3.2.4 Memo Settings

Key Type Default Description
display_with_update_time bool false Show memo's last-updated time instead of creation time
content_length_limit int 0 (unlimited) Maximum memo content length in characters
enable_double_click_edit bool false Enable double-click to edit memo inline
reactions string list Default emoji set Available emoji reactions

3.2.5 Tag Settings

Tags can have per-tag metadata stored in the instance settings:

Key Description
tag_colors.{tag}.background_color Background color for the tag chip
tag_colors.{tag}.blur_content Whether to blur memo content tagged with this tag

3.2.6 Notification (Email) Settings

Key Description
email_config.smtp_host SMTP server hostname
email_config.smtp_port SMTP server port (typically 587 for STARTTLS, 465 for SSL)
email_config.smtp_username SMTP authentication username
email_config.smtp_password SMTP authentication password
email_config.from_email Sender email address
email_config.from_name Sender display name
email_config.reply_to Reply-To email address
email_config.use_tls Enable STARTTLS
email_config.use_ssl Enable SSL/TLS (implicit TLS)

3.2.7 AI Provider Settings

Multiple AI providers can be configured simultaneously:

Key Description
ai_provider.id Unique provider identifier
ai_provider.title Display name
ai_provider.type OPENAI or GEMINI
ai_provider.endpoint API base URL (for OpenAI-compatible proxies)
ai_provider.api_key Provider API key

4. Deployment & Scaling

# Create data directory
mkdir -p /opt/memos

# Run with SQLite (default)
docker run -d \
  --name memos \
  -p 8081:8081 \
  -v /opt/memos:/var/opt/memos \
  -e MEMOS_DATA=/var/opt/memos \
  --restart unless-stopped \
  neosmemo/memos:0.26.2

# Run with PostgreSQL
docker run -d \
  --name memos \
  -p 8081:8081 \
  -e MEMOS_DRIVER=postgres \
  -e MEMOS_DSN="host=db port=5432 user=memos password=secret dbname=memos sslmode=disable" \
  -e MEMOS_INSTANCE_URL="https://memos.example.com" \
  --restart unless-stopped \
  neosmemo/memos:0.26.2

The Docker image runs as non-root uid 10001 for security hardening.

4.2 Docker Compose

# docker-compose.yml
services:
  memos:
    image: neosmemo/memos:0.26.2
    container_name: memos
    ports:
      - "8081:8081"
    volumes:
      - memos_data:/var/opt/memos
    environment:
      MEMOS_DATA: /var/opt/memos
      MEMOS_INSTANCE_URL: https://memos.example.com
    restart: unless-stopped

  # Optional: PostgreSQL backend
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: memos
      POSTGRES_USER: memos
      POSTGRES_PASSWORD: changeme
    volumes:
      - pg_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  memos_data:
  pg_data:
# docker-compose.yml with PostgreSQL backend
services:
  memos:
    image: neosmemo/memos:0.26.2
    environment:
      MEMOS_DRIVER: postgres
      MEMOS_DSN: "host=db port=5432 user=memos password=changeme dbname=memos sslmode=disable"
      MEMOS_INSTANCE_URL: https://memos.example.com
    depends_on:
      - db
    ports:
      - "8081:8081"
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: memos
      POSTGRES_USER: memos
      POSTGRES_PASSWORD: changeme
    volumes:
      - pg_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  pg_data:

4.3 Reverse Proxy (Nginx)

# /etc/nginx/sites-available/memos
server {
    listen 80;
    server_name memos.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name memos.example.com;

    ssl_certificate     /etc/letsencrypt/live/memos.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/memos.example.com/privkey.pem;

    # Increase for attachment uploads
    client_max_body_size 64m;

    location / {
        proxy_pass http://127.0.0.1:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # SSE support (HealthService stream)
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 3600s;
    }
}

Important

When running behind a reverse proxy, set --instance-url to the public URL (e.g., https://memos.example.com). This URL is used in RSS feeds, email notifications, and MCP resource references. Without it, generated links will be incorrect.

4.4 Caddy Reverse Proxy

memos.example.com {
    reverse_proxy localhost:8081 {
        header_up Host {host}
    }
    # Caddy handles TLS automatically
}

4.5 Binary Deployment (Without Docker)

# Download release binary
wget https://github.com/usememos/memos/releases/download/v0.26.2/memos_linux_amd64.tar.gz
tar xf memos_linux_amd64.tar.gz

# Run
./memos \
  --data /opt/memos \
  --port 8081 \
  --instance-url https://memos.example.com

# As a systemd service
cat > /etc/systemd/system/memos.service << 'EOF'
[Unit]
Description=Memos - Self-hosted note-taking service
After=network.target

[Service]
Type=simple
User=memos
Group=memos
ExecStart=/usr/local/bin/memos \
  --data /var/opt/memos \
  --port 8081 \
  --instance-url https://memos.example.com
Restart=on-failure
RestartSec=5s
Environment=MEMOS_DATA=/var/opt/memos

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now memos

4.6 Building from Source

# Prerequisites: Go 1.26.1+, Node.js 20+

git clone https://github.com/usememos/memos.git
cd memos
git checkout bcbcb03

# Build frontend
cd web
npm install
npm run build
# Output goes to ../server/router/frontend/dist/
cd ..

# Build binary (embeds frontend dist)
go build -o memos ./cmd/memos/main.go

# Cross-compile for Linux arm64
GOOS=linux GOARCH=arm64 go build -o memos-linux-arm64 ./cmd/memos/main.go

4.7 Unix Socket Deployment

For setups where the reverse proxy is on the same host, Unix socket is slightly more efficient than TCP:

memos --unix-sock /run/memos/memos.sock --data /var/opt/memos

# Nginx upstream
upstream memos {
    server unix:/run/memos/memos.sock;
}

Note

When --unix-sock is set, --addr and --port are ignored. Ensure the reverse proxy user has read/write permission on the socket file. See GitHub issue #5827 for reported permission edge cases with some configurations.

4.8 Scaling Considerations

Memos is designed as a single-instance application. Key limitations for horizontal scaling:

Constraint Detail
In-memory cache The store cache is process-local; multiple instances have inconsistent caches
SSE hub The SSE connection registry is in-memory; cross-instance live updates don't work
Local file storage Attachments on the local filesystem are not shared between instances
No built-in clustering No leader election, no distributed state

Recommended scaling approach: Vertical scaling (more RAM/CPU on one instance) + offload attachments to S3.

For high availability, a primary/standby setup with a shared PostgreSQL or MySQL backend and shared S3 storage is the most practical approach. Only one instance should be active at a time to avoid cache inconsistency.


5. Database Administration

5.1 Schema Overview

Table Purpose
user User accounts (id, username, role, email, nickname, password_hash, avatar_url, description, row_status)
memo Memos (id, uid, creator_id, content, visibility, pinned, payload JSON, created_ts, updated_ts, row_status)
attachment File attachments (id, uid, creator_id, filename, type, size, storage_type, reference, payload, memo_id)
memo_relation Memo-to-memo relationships (memo_id, related_memo_id, type)
reaction Emoji reactions (id, creator_id, content_id, reaction_type)
idp OAuth2 identity providers (id, uid, name, type, identifier_filter, config)
inbox User inbox messages (id, sender_id, receiver_id, status, message)
instance_setting Key-value store for all instance settings
user_setting Per-user key-value settings
memo_share Shared memo links (id, memo_id, uid, visibility, expires_at)

The row_status column on user and memo acts as a soft-delete flag: NORMAL (active) or ARCHIVED.

5.2 Migrations

Migrations run automatically at startup via store.Migrate(). There is no manual migration step.

  • SQLite: store/migration/sqlite/ — plain SQL files versioned 0.2 through 0.27
  • MySQL: store/migration/mysql/ — same version range, MySQL-compatible SQL
  • PostgreSQL: store/migration/postgres/ — same version range, PostgreSQL-compatible SQL

The current schema version is tracked in the instance_setting table under the schema_version key. Memos will not downgrade a schema; always back up before upgrading.

5.3 SQLite Administration

# Direct SQLite access (stop memos first for safety)
sqlite3 /var/opt/memos/memos.db

# Backup
sqlite3 /var/opt/memos/memos.db ".backup '/backup/memos_$(date +%Y%m%d).db'"

# Online backup (safe while running, uses SQLite backup API)
sqlite3 /var/opt/memos/memos.db "VACUUM INTO '/backup/memos_$(date +%Y%m%d).db'"

# Check integrity
sqlite3 /var/opt/memos/memos.db "PRAGMA integrity_check;"

# Reclaim space after deletions
sqlite3 /var/opt/memos/memos.db "VACUUM;"

# WAL mode (enabled by Memos automatically)
sqlite3 /var/opt/memos/memos.db "PRAGMA journal_mode;"

SQLite is opened with WAL (Write-Ahead Logging) mode and foreign keys enabled. Do not change these pragmas externally.

5.4 PostgreSQL Administration

# Create database and user
psql -U postgres << 'SQL'
CREATE USER memos WITH PASSWORD 'strongpassword';
CREATE DATABASE memos OWNER memos;
GRANT ALL PRIVILEGES ON DATABASE memos TO memos;
SQL

# Backup
pg_dump -U memos -h localhost memos | gzip > memos_$(date +%Y%m%d).sql.gz

# Restore
gunzip -c memos_20261101.sql.gz | psql -U memos -h localhost memos

# Connection string
MEMOS_DSN="host=localhost port=5432 user=memos password=strongpassword dbname=memos sslmode=require"

5.5 MySQL Administration

# Create database and user
mysql -u root -p << 'SQL'
CREATE DATABASE memos CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'memos'@'%' IDENTIFIED BY 'strongpassword';
GRANT ALL PRIVILEGES ON memos.* TO 'memos'@'%';
FLUSH PRIVILEGES;
SQL

# Backup
mysqldump -u memos -p memos | gzip > memos_$(date +%Y%m%d).sql.gz

# Connection string
MEMOS_DSN="memos:strongpassword@tcp(localhost:3306)/memos?parseTime=true&charset=utf8mb4"

5.6 Database Migration (SQLite → PostgreSQL)

There is no official automated migration tool. The recommended approach:

  1. Export data via the API (using PATs or admin credentials)
  2. Create a fresh PostgreSQL instance
  3. Re-import via the API, or use a data-transfer script against both databases directly

For large instances, a custom Go script that reads from SQLite and writes to PostgreSQL through the Store interface is the most reliable approach.


6. Operations

Note

Memos is stateless at the process level — all persistent state lives in the database and (optionally) the filesystem/S3. Restarting the process is always safe. The main operational concern is the data directory and database file.

6.1 Health Checks

# HTTP health check (no auth required)
curl http://localhost:8081/healthz
# Expected response: 200 OK with JSON body

# Container health check (Docker)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8081/healthz || exit 1

6.2 Log Management

Memos logs to stdout in plain text format. Capture logs via your container runtime or systemd:

# Docker
docker logs memos --follow

# Systemd
journalctl -u memos -f

# Filter for errors
journalctl -u memos | grep -i "error\|panic\|fatal"

There is no built-in log rotation or structured JSON logging. For production, pipe stdout to a log aggregator (Loki, Datadog, CloudWatch, etc.).

6.3 Backup Strategy

Component Backup method Frequency
SQLite DB VACUUM INTO (online) or file copy (stopped) Daily
MySQL/PostgreSQL pg_dump / mysqldump Daily
Data directory (local attachments) rsync / snapshot Daily
Instance settings Included in DB backup With DB
#!/bin/bash
# Simple SQLite backup script
BACKUP_DIR=/backup/memos
mkdir -p "$BACKUP_DIR"

# Keep 30 days
find "$BACKUP_DIR" -name "*.db" -mtime +30 -delete

sqlite3 /var/opt/memos/memos.db "VACUUM INTO '$BACKUP_DIR/memos_$(date +%Y%m%d_%H%M%S).db'"
echo "Backup complete: $BACKUP_DIR/memos_$(date +%Y%m%d_%H%M%S).db"

6.4 Upgrade Procedure

# 1. Back up data
sqlite3 /var/opt/memos/memos.db "VACUUM INTO '/backup/memos_pre_upgrade.db'"

# 2. Pull new image (Docker)
docker pull neosmemo/memos:0.26.2

# 3. Stop old container
docker stop memos && docker rm memos

# 4. Start new container (migrations run automatically)
docker run -d \
  --name memos \
  -p 8081:8081 \
  -v /opt/memos:/var/opt/memos \
  -e MEMOS_DATA=/var/opt/memos \
  --restart unless-stopped \
  neosmemo/memos:0.26.2

# 5. Verify health
curl http://localhost:8081/healthz

Note

Schema migrations run automatically on startup. Downgrading to an older version after a migration is not supported and may corrupt data. Always back up before upgrading.

6.5 User Management via API

# Set API key from user settings → Personal Access Tokens
MEMOS_API_KEY="memos_pat_..."
BASE="https://memos.example.com"

# List users (admin only)
curl -H "Authorization: Bearer $MEMOS_API_KEY" "$BASE/api/v1/users"

# Get account info
curl -H "Authorization: Bearer $MEMOS_API_KEY" "$BASE/api/v1/auth/status"

# Update instance settings (admin only)
curl -X PATCH \
  -H "Authorization: Bearer $MEMOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"setting": {"general_setting": {"disallow_user_registration": true}}}' \
  "$BASE/api/v1/instance/setting"

6.6 Background Runners

Two background goroutines run for the lifetime of the server process:

Runner Location Purpose Interval
S3 presign refresher server/runner/s3presign/ Regenerates expiring S3 presigned URLs for stored attachments Configurable (checks approaching expiry)
Memo payload updater server/runner/memopayload/ Keeps memo metadata (tags, link previews, properties) in sync with content changes Configurable

Both runners participate in the graceful shutdown sequence. They are stopped before the HTTP server closes, and the main goroutine waits for them to finish before closing the database.

6.7 SSE (Server-Sent Events)

The sse_hub.go component maintains a registry of open SSE connections for real-time updates (new memos, changes). During graceful shutdown, all SSE connections are closed before the HTTP server shuts down to prevent client-side errors.

# SSE endpoint (authenticated)
curl -N \
  -H "Authorization: Bearer $MEMOS_API_KEY" \
  "https://memos.example.com/api/v1/events"

6.8 Thumbnail Cache

Thumbnails are generated on-demand for image attachments and cached in .thumbnail_cache/ within the data directory. Generation is semaphore-limited to 3 concurrent operations to prevent CPU spikes.

# Clear thumbnail cache (safe; regenerated on demand)
rm -rf /var/opt/memos/.thumbnail_cache/

# Thumbnail specs: max 600px on longest side, JPEG output

7. Security

Warning

Memos does not implement rate limiting or request throttling at the application level. If exposed to the public internet, configure rate limiting at the reverse proxy or firewall level to prevent brute-force attacks on the login endpoint.

7.1 Authentication System

Memos uses a dual-token JWT system:

Token type Lifespan Storage Purpose
Access token 15 minutes Memory / Authorization header Short-lived API access
Refresh token Configurable (long) DB + HttpOnly cookie (memos_refresh) Renewing access tokens
Personal Access Token (PAT) No expiry DB (hash only, prefix memos_pat_) Programmatic API access

Access token flow:

  1. User signs in → server returns access token + sets memos_refresh cookie
  2. Frontend uses access token in Authorization: Bearer <token> header
  3. When access token expires, frontend calls the token-refresh endpoint
  4. Refresh token in cookie is validated against DB → new access token issued

PAT flow:

  • User generates PAT in settings → shown once, never stored in plaintext
  • PAT is hashed (bcrypt) and stored in DB
  • API requests include PAT as Authorization: Bearer memos_pat_...
  • Server hashes incoming PAT and compares to stored hash

Password hashing: bcrypt with default cost factor.

JWT signing: HMAC-SHA256 with the secret_key instance setting (auto-generated UUID on first run).

Caution

Rotating the secret_key in instance settings immediately invalidates all existing sessions and refresh tokens. All users will be logged out. PATs are stored separately and are not affected by secret_key rotation.

7.2 Authorization (ACL)

Role-based access control is enforced at the gRPC method level via acl_config.go:

Role Capabilities
Anonymous Read PUBLIC memos, RSS feeds, health check
USER Own memos/attachments CRUD, own settings, read PROTECTED memos
ADMIN All USER capabilities + user management, instance settings, IDP management

The first user registered on a fresh instance is automatically assigned the ADMIN role.

7.3 SSRF Protection

Warning

Webhook URLs are validated before delivery using SSRF protection logic in internal/webhook/. URLs resolving to reserved/private IP ranges (RFC 1918, loopback, link-local, etc.) are blocked by default. Use --allow-private-webhooks to allow webhooks to private IPs — only do this in trusted internal networks.

Blocked ranges include:

  • 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC 1918)
  • 127.0.0.0/8 (loopback)
  • 169.254.0.0/16 (link-local)
  • ::1/128, fc00::/7 (IPv6 private)

7.4 XSS Prevention

Attack vector Mitigation
SVG attachments Served as application/octet-stream, not image/svg+xml — forces download
HTML attachments Served as application/octet-stream — not rendered by browser
Custom scripts Admin-only additional_script setting; users cannot inject scripts
Markdown Rendered client-side by react-markdown with default sanitization

7.5 CSRF Protection

MCP endpoints (/mcp) validate the Origin header to prevent cross-site request forgery. The validation rejects requests whose origin does not match the instance URL.

7.6 Input Validation

Input Validation
UID fields Format validation (UUID-like pattern)
SQL queries Fully parameterized; no string interpolation
File uploads Size limit enforced (configurable upload_size_limit_mb)
S3 endpoint Standard URL parsing; path traversal not applicable

7.7 TLS

Memos does not terminate TLS. TLS must be handled by a reverse proxy (Nginx, Caddy, Traefik). For direct TLS (no reverse proxy), wrap memos with a TLS-terminating proxy on the same host.

7.8 Docker Security

The official Docker image:

  • Runs as uid 10001 (non-root)
  • Does not require --privileged
  • Needs write access only to the data volume mount point
# Verify the data directory is writable by uid 10001
chown -R 10001:10001 /opt/memos

7.9 Secrets Management

Secret Where stored Rotation impact
secret_key (JWT signing) instance_setting table Invalidates all sessions
S3 credentials instance_setting table Attachments inaccessible until updated
SMTP credentials instance_setting table Email stops until updated
AI API keys instance_setting table Transcription stops until updated
OAuth2 client secret idp table SSO login fails until updated

There is no dedicated secrets management integration (e.g., Vault, AWS Secrets Manager). All secrets are stored in the database in plaintext. Protect database access accordingly.


8. Developer Internals

8.1 Store Layer

The store layer (store/) provides the data access interface. It follows a driver interface + cache wrapper pattern:

Application code
    ↓
store.Store (interface)
    ↓
store/cache/ (in-memory cache, LRU-style)
    ↓
store/db/{sqlite,mysql,postgres}/ (SQL driver implementation)
    ↓
Database

Key design decisions:

  • All DB operations use parameterized queries — no raw string interpolation
  • WAL mode (SQLite) and FK enforcement are set on connection open
  • The cache layer wraps the driver; cache invalidation is manual (called after writes)
  • No ORM — raw SQL with database/sql

8.2 CEL Filter Engine

The filter engine in internal/filter/ implements Google's Common Expression Language (CEL) for memo queries. CEL expressions are compiled at parse time and evaluated against memo records, enabling expressive and safe server-side filtering without SQL injection risk.

This engine powers:

  • The MemoService.ListMemos filter parameter
  • MCP tool list_memos and search_memos
  • Saved shortcuts

Example filter expressions (used internally, exposed via API):

visibility == "PUBLIC"
creator == "users/1" && tag in ["work", "project"]
content.contains("meeting notes")

8.3 Markdown Processing

The backend markdown processor (internal/markdown/) extends Goldmark with custom extensions:

  • Tag parser: extracts #tag patterns into structured tag metadata stored in memo payload
  • Mention parser: extracts @username mentions for inbox notifications

The frontend uses react-markdown for rendering. The two processors must remain compatible in their interpretation of Memos-specific syntax.

8.4 Webhook System

internal/webhook/ delivers outbound webhooks when memo events occur:

  • Events: memo created, updated, deleted
  • Payload: JSON with event type + memo data
  • SSRF protection: destination URL validated before each delivery
  • No retry logic — single-attempt delivery
  • Timeout: standard Go HTTP client timeout

8.5 AI Integration

internal/ai/ provides a thin wrapper over external AI providers:

Provider Type value Feature
OpenAI OPENAI Audio transcription (Whisper), image-to-text
Google Gemini GEMINI Audio transcription, image-to-text

The AIService gRPC endpoint accepts audio or image attachments and returns transcribed text. Multiple providers can be configured; the client selects by provider ID.

8.6 MCP Server

The MCP (Model Context Protocol) server at /mcp exposes Memos as a tool-callable data source for AI assistants (Claude, etc.).

Available tools:

Tool Description
list_memos List memos with optional filters
get_memo Get a specific memo by UID
search_memos Full-text search across memos
create_memo Create a new memo
update_memo Update an existing memo
delete_memo Delete a memo
list_memo_comments List comments on a memo
create_memo_comment Add a comment to a memo
list_tags List all tags
list_attachments List attachments
get_attachment Get attachment metadata
delete_attachment Delete an attachment
link_attachment_to_memo Associate an attachment with a memo
list_memo_relations List relationships between memos
create_memo_relation Create a memo relationship
delete_memo_relation Delete a memo relationship
list_reactions List reactions on content
upsert_reaction Add or update a reaction
delete_reaction Remove a reaction

Resources: memo://memos/{uid} — individual memo content

Prompts: capture, review, daily_digest, organize

Authentication: MCP requests use the same PAT/JWT auth as the REST API. The MCP server enforces ACL identically to gRPC handlers.

CSRF: Origin header validated; requests from untrusted origins are rejected.

8.7 RSS Feeds

The RSS service (server/router/rss/) generates Atom/RSS feeds for public memos. The instance URL (--instance-url) must be set for feed links to be correct.

Feed URLs follow the pattern: GET /rss/{username} for per-user public memo feeds.

8.8 File Server

server/router/fileserver/ handles attachment binary serving with these features:

Feature Detail
Range requests Supports Range header for video/audio streaming (Safari compatibility)
Thumbnails Generated on-demand, cached in .thumbnail_cache/, max 600px, 3 concurrent
Cache-Control public, max-age=3600
SVG/HTML Served as application/octet-stream (XSS prevention)
S3 attachments Served via presigned URLs (refreshed by background runner)
Avatar serving GET /file/users/{identifier}/avatar

8.9 OAuth2 Identity Providers

internal/idp/ implements the OAuth2 authorization code flow for SSO:

  1. Admin creates an IDP record via IDPService (client ID, secret, auth URL, token URL, scopes)
  2. Users click "Sign in with {provider}" → redirected to OAuth2 authorization URL
  3. Provider redirects back with authorization code → Memos exchanges for tokens
  4. User email/identifier extracted from ID token or userinfo endpoint
  5. If identifier_filter is set on the IDP, only matching identifiers can sign in
  6. Existing user matched by email; new user created if registration is allowed

8.10 Scheduler and Cron

internal/scheduler/ provides a generic job scheduler with middleware support (logging, error recovery). internal/cron/ is a lightweight cron expression parser. These are used for the background runners but can be extended for future scheduled operations.

8.11 gRPC-Gateway and ConnectRPC

Memos uses two HTTP-to-gRPC bridges:

  • gRPC-Gateway: generates RESTful JSON endpoints at /api/v1/* from protobuf service definitions. Used by third-party API clients and curl-based integrations.
  • ConnectRPC: browser-compatible gRPC transport used by the React frontend. More efficient than gRPC-Gateway for browser clients (avoids base64 encoding overhead).

Both bridges talk to the same Go gRPC service handlers — the logic is written once in gRPC and exposed via both transports.

8.12 Version Management

internal/version/ provides version parsing and comparison utilities. The application version is embedded at build time. Migration files are compared against the stored schema_version to determine which migrations need to run.


9. Integration Points

9.1 REST API Reference

All endpoints are served at https://{instance}/api/v1/. Authentication via Authorization: Bearer {token} (access token or PAT).

Method Path Auth Description
GET /api/v1/auth/status Optional Current user info
POST /api/v1/auth/signin None Sign in with username/password
POST /api/v1/auth/signup None Register new account
POST /api/v1/auth/signout Required Sign out, revoke refresh token
GET /api/v1/memos Optional List memos (filter, pagination)
POST /api/v1/memos Required Create memo
GET /api/v1/memos/{name} Optional Get memo by name
PATCH /api/v1/memos/{name} Required Update memo
DELETE /api/v1/memos/{name} Required Delete memo
GET /api/v1/memos/{name}/comments Optional List memo comments
POST /api/v1/memos/{name}/comments Required Add comment
GET /api/v1/users Admin List users
POST /api/v1/users Admin Create user
GET /api/v1/users/{name} Optional Get user
PATCH /api/v1/users/{name} Required Update user
DELETE /api/v1/users/{name} Admin Delete user
GET /api/v1/users/{name}/stats Optional User memo statistics
GET /api/v1/attachments Required List attachments
POST /api/v1/attachments Required Upload attachment
DELETE /api/v1/attachments/{name} Required Delete attachment
GET /api/v1/instance/setting Admin Get instance settings
PATCH /api/v1/instance/setting Admin Update instance settings
GET /api/v1/identityProviders Admin List identity providers
POST /api/v1/identityProviders Admin Create identity provider
GET /api/v1/healthz None Health check

Note

The canonical API definition is the protobuf files in proto/api/v1/. The gRPC-Gateway generates HTTP/JSON bindings from them. When in doubt, the proto definition is authoritative.

9.2 Personal Access Tokens

PATs are the recommended authentication method for automation and integrations:

# Generate via UI: Settings → Personal Access Tokens → Create

# Use in API calls
curl -H "Authorization: Bearer memos_pat_..." \
  https://memos.example.com/api/v1/memos

# Use with MCP clients (e.g., Claude Desktop)
# In claude_desktop_config.json:
{
  "mcpServers": {
    "memos": {
      "command": "curl",
      "args": ["-N", "https://memos.example.com/mcp"],
      "headers": {
        "Authorization": "Bearer memos_pat_..."
      }
    }
  }
}

9.3 MCP Integration

Configure Memos as an MCP data source for AI assistants:

// Claude Desktop MCP configuration
{
  "mcpServers": {
    "memos": {
      "type": "http",
      "url": "https://memos.example.com/mcp",
      "headers": {
        "Authorization": "Bearer memos_pat_your_token_here"
      }
    }
  }
}

9.4 Webhooks

Configure webhooks in user settings to receive event notifications:

// Webhook payload shape (memo created)
{
  "event": "memo.created",
  "memo": {
    "name": "memos/abc123",
    "content": "Note content...",
    "visibility": "PRIVATE",
    "createTime": "2026-05-07T10:00:00Z"
  }
}

Warning

Webhooks are single-attempt with no retry. If the destination is unreliable, implement idempotent handling and use a queuing layer on the receiving end. Private IP destinations are blocked by default (SSRF protection).

9.5 RSS Feeds

# Per-user public memo feed
GET /rss/{username}

# Example
https://memos.example.com/rss/alice

Feeds contain all PUBLIC memos for the specified user. The instance URL must be configured (--instance-url) for feed links to resolve correctly.

9.6 S3-Compatible Storage Integration

# MinIO example
# Instance settings (via admin UI or API):
{
  "storage_type": "S3",
  "s3_config": {
    "endpoint": "https://minio.example.com",
    "region": "us-east-1",
    "bucket": "memos-attachments",
    "access_key_id": "minioadmin",
    "access_key_secret": "minioadmin",
    "use_path_style": true
  }
}

# Cloudflare R2 example
{
  "storage_type": "S3",
  "s3_config": {
    "endpoint": "https://<account>.r2.cloudflarestorage.com",
    "region": "auto",
    "bucket": "memos",
    "access_key_id": "<R2 access key>",
    "access_key_secret": "<R2 secret>",
    "use_path_style": false
  }
}

9.7 OAuth2 SSO Configuration

# Create IDP via API (admin)
curl -X POST \
  -H "Authorization: Bearer $ADMIN_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "identityProvider": {
      "name": "Google SSO",
      "type": "OAUTH2",
      "config": {
        "client_id": "xxx.apps.googleusercontent.com",
        "client_secret": "xxx",
        "auth_url": "https://accounts.google.com/o/oauth2/auth",
        "token_url": "https://oauth2.googleapis.com/token",
        "scopes": ["openid", "email", "profile"],
        "field_mapping": {
          "identifier": "email",
          "display_name": "name"
        }
      }
    }
  }' \
  https://memos.example.com/api/v1/identityProviders

10. Troubleshooting

Tip

Before investigating application-level issues, verify the health endpoint responds: curl http://localhost:8081/healthz. A non-200 response or connection refused means the process is not running or not bound to the expected address.

10.1 Common Issues

Error Cause Solution Prevention
address already in use on startup Port 8081 already occupied Change --port or stop the conflicting process Use a dedicated port; check with ss -tlnp | grep 8081
permission denied on data directory Process user lacks write access chown -R <uid>:<gid> /data/dir Set ownership before first run; Docker: uid 10001
database is locked (SQLite) Multiple processes opening same SQLite file Run only one Memos instance per SQLite file Use PostgreSQL/MySQL for multi-process access
no such file or directory for DB Data directory does not exist Create the directory before starting Memos Pre-create data dir in Dockerfile/systemd
invalid DSN Malformed connection string Check DSN format for your driver Test DSN with driver CLI before configuring
Login loop after upgrade secret_key changed or refresh token invalidated Clear memos_refresh cookie; log in again Never manually edit secret_key
Attachments not loading after storage type change Old attachments reference old storage type Migrate attachments manually; do not switch storage types on existing data without migration Plan storage type before first use
S3 presigned URLs expiring S3 presign runner not running or S3 credentials changed Check runner logs; update credentials in instance settings Monitor S3 credential expiry separately
Webhooks not delivered to internal URLs SSRF protection blocking private IPs Use --allow-private-webhooks flag Only enable on trusted internal networks
MCP requests rejected with 403 Invalid Origin header Ensure MCP client sends correct Origin matching instance URL Set --instance-url correctly
iOS PWA empty feed Known issue #5823 Check for updates; workaround: reload in browser —
Auto-refresh errors Known issue #5607 Check logs for token refresh failures; re-login if persistent —
Unix socket permission denied Socket file permissions Ensure reverse proxy user can read/write socket See GitHub issue #5827
Masonry layout not persisted Known issue #5807 Layout preference may reset on page reload —
Calendar past months wrong color Known issue #5816 Cosmetic; no data impact —

Tip

When troubleshooting attachment issues, always check the storage_type setting first. A mismatch between stored storage_type on individual attachment records and the current instance-level setting is the most common cause of broken attachment links.

10.2 Database Diagnostics

# SQLite: check for corruption
sqlite3 /var/opt/memos/memos.db "PRAGMA integrity_check;"

# SQLite: check WAL status
sqlite3 /var/opt/memos/memos.db "PRAGMA journal_mode; PRAGMA wal_checkpoint;"

# Check schema version
sqlite3 /var/opt/memos/memos.db \
  "SELECT value FROM instance_setting WHERE name = 'schema_version';"

# List all users
sqlite3 /var/opt/memos/memos.db \
  "SELECT id, username, role, row_status FROM user;"

# Count memos by visibility
sqlite3 /var/opt/memos/memos.db \
  "SELECT visibility, COUNT(*) FROM memo GROUP BY visibility;"

10.3 Authentication Diagnostics

# Decode JWT (for debugging — do not use in production)
# JWT is base64-encoded; inspect payload
echo "eyJ..." | cut -d. -f2 | base64 -d | jq .

# Check refresh token in cookie
# Browser DevTools → Application → Cookies → memos_refresh

# Revoke all sessions for a user (admin — delete refresh tokens from DB)
sqlite3 /var/opt/memos/memos.db \
  "DELETE FROM user_setting WHERE user_id = <uid> AND key = 'refresh_token';"

10.4 Performance Diagnostics

# Check memo count
sqlite3 /var/opt/memos/memos.db "SELECT COUNT(*) FROM memo WHERE row_status = 'NORMAL';"

# Check attachment storage usage
sqlite3 /var/opt/memos/memos.db "SELECT storage_type, COUNT(*), SUM(size)/1024/1024 AS total_mb FROM attachment GROUP BY storage_type;"

# Check thumbnail cache size
du -sh /var/opt/memos/.thumbnail_cache/

# Docker resource usage
docker stats memos --no-stream

11. Module Reference

11.1 cmd/memos/main.go

Entry point. Registers the root Cobra command with all CLI flags. Initializes the profile from flags/env, calls store.Migrate(), constructs the Store, constructs the Server, and calls server.Start(). Signal handling (SIGTERM/SIGINT) triggers graceful shutdown.

11.2 server/server.go

Creates and configures the Echo HTTP server. Responsibilities: middleware registration (recovery), health endpoint, static file embedding, gRPC server setup, gRPC-Gateway bridge, file server routes, RSS routes, MCP registration, background runner startup, graceful shutdown coordination.

11.3 server/auth/

Implements the Authenticator middleware. Extracts credentials from Authorization: Bearer header (both PATs and JWTs) and the memos_refresh HttpOnly cookie. Validates JWTs against the secret_key. For PATs: bcrypt hash comparison against DB. Populates the Echo context with the resolved user identity for downstream handlers.

11.4 server/router/api/v1/

Contains all gRPC service handler implementations. One Go file per service (e.g., memo_service.go, user_service.go). acl_config.go defines which gRPC methods require which roles. sse_hub.go manages the in-memory SSE connection registry.

11.5 server/router/fileserver/

Native HTTP handler for GET /file/attachments/{uid}/{filename} and GET /file/users/{identifier}/avatar. Implements:

  • Content-type detection with SVG/HTML override to application/octet-stream
  • Range request parsing and response for video/audio streaming
  • On-demand thumbnail generation with .thumbnail_cache/ persistence
  • Cache-Control: public, max-age=3600 header
  • S3 presigned URL redirect for S3-stored attachments

11.6 server/router/frontend/

Serves the embedded React SPA. The frontend build output (web/dist/) is embedded into the binary at compile time via Go's embed package. All routes not matched by other handlers return index.html, enabling client-side routing.

11.7 server/router/mcp/

Implements the MCP server using the mcp-go library. Registers all tools, resources, and prompts. Enforces CSRF via Origin header validation. All tool calls go through the same Store layer as gRPC handlers.

11.8 server/router/rss/

Generates RSS/Atom XML feeds for public memos. Reads PUBLIC memos from the Store, formats them as feed items, and returns the feed document.

11.9 store/

The data access interface (Store) is defined here. Implementations in store/db/{sqlite,mysql,postgres}/ provide driver-specific SQL. The cache wrapper in store/cache/ intercepts reads (returning cached values) and writes (invalidating cache entries). Migrations in store/migration/ are plain SQL files, one per schema version.

11.10 internal/filter/

CEL (Common Expression Language) filter engine. Takes a filter string, compiles it to a CEL program, and evaluates it against memo records. Used by MemoService.ListMemos and MCP tools to implement server-side memo filtering without ad-hoc SQL construction.

11.11 internal/markdown/

Goldmark-based markdown processor. Custom extensions:

  • Tag extractor: parses #tag tokens, extracts tag names, stores in memo payload JSON
  • Mention extractor: parses @username tokens, creates inbox notifications

11.12 internal/webhook/

Outbound webhook delivery. Before making the HTTP request, validates the destination URL against reserved CIDR blocks (SSRF protection). Single-attempt delivery with standard HTTP timeout. No retry queue.

11.13 internal/ai/

Thin client wrappers for OpenAI and Google Gemini APIs. Used by AIService for audio transcription and image-to-text. Provider selection is by configured provider ID. API keys are stored in instance settings.

11.14 internal/email/

SMTP email client. Supports STARTTLS and implicit SSL/TLS. Used for account notifications. Configuration in instance settings (email_config.*). No email queuing — delivery is synchronous.

11.15 internal/idp/

OAuth2 identity provider client. Implements the authorization code flow: generates auth URL, exchanges code for tokens, fetches user info, applies identifier_filter. Supports any standard OAuth2 provider.

11.16 internal/profile/

Defines the Profile struct that holds the resolved runtime configuration (data dir, addr, port, driver, DSN, instance URL, etc.). Populated from CLI flags at startup and passed to all major components.

11.17 internal/storage/s3/

S3 client wrapper using the AWS SDK. Handles upload, download (presigned URL generation), and deletion. Used by both the file server (for serving) and the S3 presign runner (for URL refresh).

11.18 web/

React 18 SPA built with Vite and TypeScript. Key libraries: Tailwind CSS 4 (styling), Radix UI (headless components), TanStack React Query (server state), react-markdown (markdown rendering), ConnectRPC (gRPC transport). The build output is embedded into the Go binary at compile time.