Documentation pages

Configuration#

Groundnote is configured by one JSON file without secrets and by environment variables for everything secret.

Groundnote reads groundnote.json (path from GROUNDNOTE_CONFIG, default ./groundnote.json). The file contains no secrets. It is validated at start; invalid configuration refuses to start.

{
  "instance": "my-company",
  "zone": "Europe/Berlin",
  "public_url": "https://groundnote.example.org",
  "scopes": [
    { "name": "sales", "providers_allowed": [] }
  ],
  "access": {
    "team_domain": "https://my-team.cloudflareaccess.com",
    "aud_mcp": "<audience tag of the MCP application>",
    "aud_approval": "<audience tag of the approval application>",
    "aud_api": "<audience tag of the API application>"
  },
  "search": {
    "synonyms": [["hook", "bracket", "hanger"]],
    "languages": ["de", "en"],
    "min_coverage": 0,
    "semantic": { "enabled": false },
    "reranker": { "enabled": false }
  },
  "recheck_after_days": 180,
  "approval": { "mode": "classified" },
  "backup": {
    "kind": "litestream",
    "target": "https://<account-id>.r2.cloudflarestorage.com/<bucket>",
    "prefix": "groundnote-prod",
    "deadman_url": "https://<your dead-man switch ping URL>"
  }
}

search.semantic ({enabled, model: "bge-m3" | "qwen3-embedding-0.6b", model_dir}) switches on stage b for every scope whose providers_allowed names local; fetch the model once with groundnote model fetch and check it with groundnote doctor. search.reranker ({enabled, monthly_cap_usd, timeout_ms, threshold}) switches on stage c for scopes with reranker: true and typesafe allowed. Keys and defaults: search/stages-b-c.md §8.

The scopes list is what may exist. A scope file is only created by groundnote scope create <name>. recheck_after_days (30 to 730) controls when a rule is shown with "still valid?". approval.mode is autonomous, classified (the project default shown here) or manual; without the approval block every scope runs manual, and the mandatory exceptions are never configurable (approval-modes.md §3). The access block is only used in cloudflare-access mode. backup.kind is litestream (needs target, prefix, deadman_url) or copy (needs directory, deadman_url); see backup.md. Unknown keys fail the start. The file in the example must match your auth mode: access is required in cloudflare-access mode, together with an https public_url.

Variable Meaning
GROUNDNOTE_AUTH_MODE cloudflare-access or dev-token. No default: without it the server refuses to start.
GROUNDNOTE_CONFIG Path to groundnote.json (default ./groundnote.json).
GROUNDNOTE_DATA_DIR Data directory (default ./data). Created 0700; SQLite files are created 0600. The server refuses to start (data-dir-permissions) if the directory, scopes/ or any SQLite file in them is group- or world-accessible; fix with chmod 700 on directories, chmod 600 on files. Holds the ops socket ops.sock. With a bind mount instead of a named volume, the host directory must be owned by uid 10001 and have mode 0700.
GROUNDNOTE_SESSION_SECRET Signs approval sessions and CSRF tokens.
GROUNDNOTE_HOST, GROUNDNOTE_PORT Public listener (default 127.0.0.1, 8794).
GROUNDNOTE_OPS_TOKEN Required, at least 32 characters. Bearer for the ops socket used by the groundnote CLI. GROUNDNOTE_OPS_PORT no longer exists; setting it refuses the start.
GROUNDNOTE_DEV_MCP_TOKEN Dev-token mode: bearer for /mcp. At least 32 characters.
GROUNDNOTE_DEV_APPROVAL_TOKEN Dev-token mode: password for the approval login. At least 32 characters, different from the MCP token.
GROUNDNOTE_DEV_IDENTITY Dev-token mode: email of the single approving person.
GROUNDNOTE_DEV_ALLOW_NON_LOOPBACK Dev-token mode: allow a non-loopback bind (container use only).
TYPESAFE_API_KEY Stage c only: required as soon as a scope can run the reranker (search.reranker.enabled, reranker: true and typesafe in its providers_allowed); otherwise the start is refused with reranker-key-missing.

The default bind is 127.0.0.1:8794; any two secrets that are equal, or too short, refuse the start. Secrets come only from the environment, never from the config file, and are never logged.

The CLI#

groundnote help lists every command; the full reference is cli.md. The api commands use the caller's own credential (GROUNDNOTE_URL with GROUNDNOTE_API_TOKEN, or cli.json from groundnote config init). The ops commands talk to the ops listener on the Unix socket ops.sock in the data directory (mode 0600, no TCP port) and read GROUNDNOTE_DATA_DIR and GROUNDNOTE_OPS_TOKEN; they refuse to send the token to a socket that another user owns. groundnote doctor checks the setup and names a remedy for each finding. In dev-token mode, never publish the server port on a public interface.

Production: Cloudflare Access#

In production run Groundnote behind Cloudflare Access (GROUNDNOTE_AUTH_MODE=cloudflare-access) with Managed OAuth enabled on the MCP application. Groundnote verifies the Cf-Access-Jwt-Assertion header (RS256 against your team's JWKS, issuer and audience) and serves /.well-known/oauth-protected-resource (RFC 9728) so clients can discover the login. Use three Access applications with separate audiences for /mcp, /approval and the rest of /v1/*; an MCP token is rejected on the approval routes. Dev-token mode with a bearer token is for local tests only.

Backup#

Each scope is one SQLite file in WAL mode. The service never schedules a backup. groundnote backup nightly (copy, restore probe with the nightly export, then the dead-man ping to backup.deadman_url) is meant for one OS timer a day (cli.md §8); the service does not start or supervise Litestream yet. Around it: groundnote export <scope> (complete Markdown archive, persons with decide only), groundnote backup verify (compares the newest backup with the live scope) and groundnote restore --to <file>. Choose one way per instance with backup.kind: copy (consistent copies into a directory that restic or similar picks up; encrypted client-side by that tool, the recommended default) or litestream (continuous replication to S3-compatible storage for a low recovery point). Groundnote does not encrypt backups or exports itself yet (planned with redaction). Litestream 0.5.x cannot encrypt client-side: replicated pages are plaintext to the bucket operator, so use it only with a trusted bucket and bucket-side encryption. The container image ships sqlite3 and Litestream. See backup.md and examples/litestream.yml.

Edit on GitHub