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.