# your domain, e.g https://example.com APP_URL=http://localhost:3000 PORT=3000 # --- Security / reverse proxy --- # The app derives the client IP (req.ip) from the `X-Forwarded-For` header via # Fastify `trustProxy`. That header is client-forgeable, so XFF is trusted only # from proxies on the configured trusted networks. Deploy this app behind a # trusted reverse proxy that SETS/OVERWRITES (not appends) `X-Forwarded-For` # with the real client IP. If XFF is trusted from an untrusted source, any # per-IP throttling — including the /mcp Basic brute-force limiter — can be # bypassed by an attacker who simply spoofs `X-Forwarded-For` to rotate IPs. # (The /mcp limiter keeps a global per-email key as an IP-independent backstop, # but the per-IP and per-IP+email keys rely on a trustworthy X-Forwarded-For.) # # TRUST_PROXY controls which proxies are trusted to set X-Forwarded-For. # Default (unset/empty): `loopback, linklocal, uniquelocal` — XFF is trusted # ONLY from private/loopback proxies, so a public-IP client cannot spoof req.ip. # This is the safe default for the common case where the reverse proxy runs on # loopback or a private network; req.ip still resolves to the real client. # WARNING: this changed the previous default of trust-all. If your reverse proxy # sits on a PUBLIC IP, the default will NOT trust its XFF and req.ip will be the # proxy's IP — set TRUST_PROXY accordingly. Accepted values: # - true restore trust-all (ONLY safe if a trusted proxy ALWAYS overwrites # X-Forwarded-For; otherwise clients can spoof their IP) # - false never trust X-Forwarded-For (req.ip is the socket peer) # - number of trusted proxy hops in front of the app # - comma-separated CIDR/IP list of trusted proxies, e.g. # `127.0.0.1, 10.0.0.0/8` # TRUST_PROXY= # APP_SECRET has a DUAL role: it signs JWTs AND derives the AES-256-GCM key that # encrypts stored AI-provider credentials (API keys) at rest. CONSEQUENCE: if you # change APP_SECRET after setup, every stored AI API key becomes undecryptable — # you must re-enter them in AI settings — and all existing sessions/JWTs are # invalidated. Choose it ONCE, keep it stable, and back it up alongside your DB. # minimum of 32 characters. Generate one with: openssl rand -hex 32 APP_SECRET=REPLACE_WITH_LONG_SECRET JWT_TOKEN_EXPIRES_IN=30d DATABASE_URL="postgresql://postgres:password@localhost:5432/docmost?schema=public" REDIS_URL=redis://127.0.0.1:6379 # options: local | s3 | azure STORAGE_DRIVER=local # S3 driver config AWS_S3_ACCESS_KEY_ID= AWS_S3_SECRET_ACCESS_KEY= AWS_S3_REGION= AWS_S3_BUCKET= AWS_S3_ENDPOINT= AWS_S3_FORCE_PATH_STYLE= # Azure Blob Storage driver config AZURE_STORAGE_ACCOUNT_NAME= AZURE_STORAGE_ACCOUNT_KEY= AZURE_STORAGE_CONTAINER= # default: 50mb FILE_UPLOAD_SIZE_LIMIT= # options: smtp | postmark MAIL_DRIVER=smtp MAIL_FROM_ADDRESS=hello@example.com MAIL_FROM_NAME=Docmost # SMTP driver config SMTP_HOST=127.0.0.1 SMTP_PORT=587 SMTP_USERNAME= SMTP_PASSWORD= SMTP_SECURE=false SMTP_IGNORETLS=false # Postmark driver config POSTMARK_TOKEN= # for custom drawio server DRAWIO_URL= # Gotenberg URL for server-side PDF export GOTENBERG_URL= DISABLE_TELEMETRY=false # Allow other sites to embed Docmost in an iframe. IFRAME_EMBED_ALLOWED=false # Only used when IFRAME_EMBED_ALLOWED=true. When empty, any origin is allowed. # Example: https://intranet.example.com,https://portal.example.com IFRAME_ALLOWED_ORIGINS= # Enable debug logging in production (default: false) DEBUG_MODE=false # Log database queries DEBUG_DB=false # Log http requests LOG_HTTP=false # MCP server (community): the embedded /mcp endpoint authenticates PER USER. # An MCP client authenticates with one of: # - HTTP Basic: `Authorization: Basic base64(email:password)` — the user's own # Docmost login/password. The server validates the credentials and the MCP # session then acts under that user's permissions (edits attributed to them). # - Bearer access JWT: `Authorization: Bearer ` (the user's # `authToken` cookie value). Validated as an ACCESS token. # # OPTIONAL service-account fallback. When a request carries NEITHER Basic NOR # Bearer credentials and these are set, the MCP session falls back to this # shared service account (back-compat; useful for CI/scripts). Leave BLANK to # require per-user credentials. MCP_DOCMOST_EMAIL= MCP_DOCMOST_PASSWORD= # MCP_DOCMOST_API_URL=http://127.0.0.1:3000/api # Optional shared guard for the /mcp endpoint. When set, every /mcp request must # carry a matching `X-MCP-Token` header (separate from `Authorization`, which now # carries the per-user credentials). When unset, /mcp relies on the per-user # credentials above plus the workspace MCP toggle and network isolation (do not # expose the port publicly). # MCP_TOKEN= # MCP_SESSION_IDLE_MS=1800000 # # AI-AGENT ATTRIBUTION (comments/pages written via MCP are badged as "AI"): # attribution is driven by a per-user `is_agent` flag on the users row. There is # NO admin UI/API for it — set it out-of-band with SQL. Use a DEDICATED service # account for the MCP fallback above and flag ONLY that account, e.g.: # UPDATE users SET is_agent = true WHERE email = 'mcp-bot@your-domain'; # NEVER set is_agent on a human or shared account — every action by that account # (including normal human edits) would then be mis-attributed as AI. # Agent-roles catalog source: an http(s):// base URL to the catalog's raw files # (the server appends /index.json and /bundles//.json). This value is # baked into the Docker image at build time per branch (see the Dockerfile ARG # AI_AGENT_ROLES_CATALOG_URL and the CI build-args). Set it here only to point a # local/non-Docker run at a catalog; if unset, the "import role from catalog" # admin feature is unavailable. Local-filesystem sources are no longer supported. # AI_AGENT_ROLES_CATALOG_URL= # Per-embedding-call timeout in milliseconds for the RAG indexer. # A slow/hung embeddings endpoint fails after this and the batch continues. # AI_EMBEDDING_TIMEOUT_MS=120000 # Silence timeout (ms) for streaming chat/agent AI calls AND external-MCP traffic. # Bounds time-to-first-byte and the gap BETWEEN chunks (NOT the total turn length), # so an arbitrarily long turn that keeps streaming is never cut. Finite so a hung # provider is eventually broken instead of leaking forever. Default 900000 (15 min). # AI_STREAM_TIMEOUT_MS=900000 # Keep-alive recycle window (ms) for streaming chat/agent AI + external-MCP calls. # A pooled connection idle longer than this is closed instead of reused, so a # NAT / egress firewall / reverse proxy that silently drops idle connections # cannot poison a reused socket into a PRE-RESPONSE `read ECONNRESET`. Lower it if # your egress drops idle connections faster than ~10s. Default 10000 (10 s). # AI_STREAM_KEEPALIVE_MS=10000 # Silence timeout (ms) for EXTERNAL-MCP transport ONLY (not the chat provider). # Tighter than AI_STREAM_TIMEOUT_MS so a byte-silent/hung MCP server is broken in # ~5 min instead of 15. Note it also cuts a legitimately long but byte-silent # single tool call (a slow crawl that emits nothing until done) and an SSE # transport idling >5 min BETWEEN tool calls. Default 300000 (5 min). # AI_MCP_STREAM_TIMEOUT_MS=300000 # Total wall-clock cap (ms) for ONE external MCP tool call (app-level, not # transport). Aborts a tool that keeps the socket warm (SSE heartbeats / trickle) # but never returns a result — which the silence timeout above never breaks. # Default 900000 (15 min). # AI_MCP_CALL_TIMEOUT_MS=900000 # --- Anonymous public-share AI assistant --- # Opt-in per workspace (AI settings -> "public share assistant"; off by default). # When enabled, anonymous visitors of a published share can ask an AI about that # share at POST /api/shares/ai/stream. The assistant is read-only and hard-scoped # to the single share tree, but every call spends real tokens on the workspace # owner's configured AI provider. # # DEPLOYMENT REQUIREMENT: the per-IP rate limit on this endpoint is only # effective behind a trusted reverse proxy that OVERWRITES (not appends) # X-Forwarded-For with the real client IP. The app runs with trustProxy, so # without such a proxy an attacker can rotate X-Forwarded-For to evade the # per-IP limit. Put this endpoint (and the app) behind a proxy you control that # sets X-Forwarded-For to the real client IP. # # Backstop: a cluster-wide, sliding-window cap per workspace (IP-independent, # keyed by the server-resolved workspace id) bounds the owner's bill even if the # per-IP limit is fully evaded. It is a COST backstop, not an access control, and # FAILS CLOSED if Redis is unavailable (an optional assistant briefly going # offline is safer than an unbounded bill). Override the hourly cap below # (default: 100 calls per workspace per rolling hour). # SHARE_AI_WORKSPACE_MAX_PER_HOUR=100 # # Per-request output-token ceiling for the anonymous assistant (default: 512). # Worst-case output per accepted call = agent steps (5) × this value. # SHARE_AI_MAX_OUTPUT_TOKENS=512 # # Second cost backstop: a cluster-wide per-workspace rolling-DAY token budget # (input re-sent per step + output, summed across every accepted turn). The # hourly request cap above bounds how MANY calls run, not how expensive each is, # so this caps the owner's actual provider bill directly. Like the request cap it # FAILS CLOSED if Redis is unavailable (default: 1,000,000 tokens per workspace # per rolling day). # SHARE_AI_WORKSPACE_TOKEN_BUDGET_PER_DAY=1000000 # --- GIT-SYNC (native two-way Docmost <-> git Markdown sync) --- # Master switch. Off by default. When 'true', GIT_SYNC_SERVICE_USER_ID below is # REQUIRED (the service account that git-originated create/move/rename/delete are # attributed to) — the server refuses to boot with sync enabled and no user id. # GIT_SYNC_ENABLED=false # # Serve the per-space vaults over smart-HTTP (the /git host). Defaults to # GIT_SYNC_ENABLED when unset. # GIT_SYNC_HTTP_ENABLED=false # # REQUIRED when GIT_SYNC_ENABLED=true: id of the user that git-originated page # operations (create / move / rename / delete) are attributed to. # GIT_SYNC_SERVICE_USER_ID= # # Where the per-space bare repos / working vaults live. # Defaults to "/git-sync". # GIT_SYNC_DATA_DIR= # # Optional remote URL template to mirror each space's vault to (e.g. a git host). # Leave unset to keep vaults local-only. # GIT_SYNC_REMOTE_TEMPLATE= # # Path to the SSH private key used when pushing to GIT_SYNC_REMOTE_TEMPLATE. # GIT_SYNC_SSH_KEY_PATH= # # Poll-safety interval in ms — the cadence of the background reconcile cycle # (default: 15000). # GIT_SYNC_POLL_INTERVAL_MS=15000 # # Debounce window in ms for collapsing bursts of page edits into one sync cycle # (default: 2000). # GIT_SYNC_DEBOUNCE_MS=2000 # # Defense-in-depth absolute cap on soft-deletes applied per push cycle # (default: 5). A non-convergent / phantom-absence cycle can never trash more # than this many pages without an explicit override. # GIT_SYNC_MAX_DELETES_PER_CYCLE=5