mcp server
canterbury
An experimental system for connecting AI agents to an Obsidian vault through a controlled service layer.
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 23, 2026Canterbury
Canterbury is an experimental system for connecting AI agents to an Obsidian vault through a controlled service layer. The long-term goal is to let agents read, search, and eventually write vault content while enforcing explicit access policies and recording an independent audit trail of every interaction.
This repository currently implements a Dockerized sync worker, a local Go vault
service with scoped ReadNote and SearchNotes RPCs, a stateless HTTP MCP
gateway for those RPCs, and local authentication tooling.
Project Status
Canterbury is in early development. The current implementation includes:
- A Node-based sync worker container.
obsidian-headlessintegration.- Docker Compose configuration.
- A local Go vault service that reads and searches scoped Markdown notes from a filesystem vault mirror.
- Local Docker Compose stack with Dex, Pomerium Core, and the vault service for testing the deployed-style auth gateway flow.
- JWT-based request authentication with JWKS verification and TOML scope mappings.
- Scope-based authorization using note-declared access scopes and authenticated principals.
- Date-rotated JSONL audit logging for vault read/search attempts and authentication failures.
- Connect/gRPC health, reflection,
ReadNote, andSearchNoteshandlers. - A stateless Streamable HTTP MCP gateway exposing the allowlisted
read_noteandsearch_notestools atPOST /mcp, with internal liveness and readiness endpoints atGET /health/liveandGET /health/ready. - A development auth CLI that starts a local Connect/gRPC service for minting local JWTs and serving its public verification key as JWKS.
- Repository formatting, test, and linting tooling.
Planned or incomplete components include:
- Indexing and plugin-style vault operations.
- Production identity-provider runbooks and policy-management workflows beyond the local Pomerium/Dex and development auth helpers.
Project Description
Canterbury aims to act as secure middleware between AI agents and an Obsidian vault. AI agents should not read or write vault files directly. Instead, agents will interact with a service that authorizes requests, exposes structured query interfaces, and records actions outside the vault.
The sync worker is the trusted component that mirrors the vault. The vault service consumes that mirrored vault without receiving Obsidian account credentials.
Who This Project Is For
Use Canterbury if you want to explore AI-assisted workflows over an Obsidian vault while keeping clear boundaries around access, authorization, and audit records.
The current sync worker is useful if you want to run Obsidian Sync in Docker on a server, NAS, homelab machine, or cloud container environment.
The current vault service and MCP gateway are useful for local development of controlled, read-only agent access over a filesystem mirror of the vault. Indexing, write workflows, MCP-native OAuth discovery, and production identity-provider integration are still planned.
Canterbury is not a replacement for an Obsidian Sync subscription. The sync worker requires valid Obsidian Sync credentials and an existing remote vault.
Architecture
The intended system has several components:
- Sync worker: Mirrors an Obsidian vault using
obsidian-headless. - Vault service: Provides the primary controlled interface to vault data.
- MCP server: Adapts an explicit allowlist of vault RPCs into stateless Streamable HTTP tools and forwards each request's bearer assertion.
- Authorization and classification: Grants access only to explicitly scoped notes when the caller principal has matching scopes.
- Audit system: Records operations outside the vault in an append-only log.
- Indexing layer: Parses notes into searchable metadata.
- Plugin and operation framework: Runs extensible processing over vault events and content.
The current repository implements the sync worker, the vault service read and search paths, the read-only MCP gateway, JWT-authenticated vault RPCs, a local Pomerium/Dex gateway stack, a development JWT issuer, and filesystem JSONL audit logging for read, search, and authentication failure events. Indexing and write workflows are not implemented yet.
See Canterbury Architecture for the planned Go package structure and dependency boundaries. See Auth V1 Design and Audit System Design for the implemented authentication and audit boundaries.
Access Model
The access model is default deny. Notes opt in to controlled service exposure by declaring access scopes in frontmatter:
access:
scopes:
- personal-agent
- public-site
Missing access.scopes means the note is not available through controlled
service interfaces. The current vault service validates signed bearer JWTs,
maps each token issuer and subject to Canterbury scopes from a TOML file outside
the vault, and authorizes note access from those mapped scopes.
Valid JWTs whose (issuer, subject) pair is not present in the mapping file are
rejected as unauthenticated because Canterbury cannot resolve them to a local
principal. Mapped principals whose scopes do not match a note's declared access
scopes are authenticated but denied by vault policy.
Project Dependencies
To run the current sync worker, you need:
- Docker with Docker Compose.
- An Obsidian account with Sync enabled.
- An Obsidian auth token.
- The name or ID of the remote vault to sync.
- The end-to-end encryption password for the remote vault.
To develop Canterbury or run the local vault service, you also need:
- Go 1.26 or newer.
golangci-lint2.11.4 or a compatible 2.x release.- Node.js 24 or a compatible version supported by the sync dependencies.
- npm.
- Make.
Install development dependencies and tools:
make setup
Run the full repository check:
make check
Deployment Images
Canterbury publishes Linux/amd64 deployment images for its MCP server, vault service, and sync worker to GHCR. Images receive immutable full-commit SHA tags; production deployments must use the reported manifest digest. See Publishing Canterbury Images for package names, manual build and publish modes, provenance/SBOM verification, and the required post-publish public-package and anonymous-pull checks.
Install Canterbury
- Clone this repository.
- Generate local Pomerium config, certificates, and secrets:
scripts/setup-local-pomerium.mts
- Build the default local stack:
CANTERBURY_UID="$(id -u)" CANTERBURY_GID="$(id -g)" docker compose build
Configure The Sync Worker
This step is only required when running the optional sync profile.
Copy the example environment file:
cp sync/.env.example sync/.env
Edit sync/.env and set the required values:
SYNC_VAULT_NAME=your-vault-name-or-id
SYNC_VAULT_PASSWORD=your-vault-encryption-password
SYNC_OBSIDIAN_AUTH_TOKEN=your-obsidian-auth-token
Optional values:
SYNC_DEVICE_NAME=canterbury-sync
SYNC_VAULT_PATH=/vault
| Variable | Required | Description |
|---|---|---|
SYNC_VAULT_NAME | Yes | Remote Obsidian vault name or ID. |
SYNC_VAULT_PASSWORD | Yes | End-to-end encryption password for the remote vault. |
SYNC_OBSIDIAN_AUTH_TOKEN | Yes | Obsidian auth token used by obsidian-headless. |
SYNC_DEVICE_NAME | No | Device name shown in Obsidian Sync history. Defaults to canterbury-sync. |
SYNC_VAULT_PATH | No | Local vault path inside the container. Defaults to /vault in Compose. |
Do not commit sync/.env. This repository ignores it because it contains
secrets.
Run The Sync Worker
Start the sync worker:
docker compose --profile sync up --build obsidian-sync
Run it in the background:
docker compose --profile sync up --build -d obsidian-sync
Stop it:
docker compose down
The service uses the obsidian-vault Docker volume by default:
volumes:
- obsidian-vault:/vault
This is the portable default. If you replace it with a host bind mount such as
./vault:/vault, you must ensure the container user can write to that host
directory.
Sync Worker Readiness And Lifecycle
The sync worker deliberately separates startup progress from readiness. It
validates its environment, writes the Obsidian token to its private config
file, configures the vault when needed, and runs a one-time ob sync. Only
after that initial sync succeeds does it start ob sync --continuous and
report ready.
The image includes its own credential-free probe:
docker compose --profile sync exec obsidian-sync /app/sync.js healthcheck
docker compose --profile sync exec obsidian-sync /app/sync.js healthcheck --mode live
The command defaults to --mode ready. A successful command prints only
READY or LIVE and exits with status 0; an unsuccessful command prints
NOT_READY or NOT_LIVE and exits with status 1.
| Mode | Contract |
|---|---|
live | The supervisor heartbeat is fresh, shutdown has not begun, and the active ob child is alive when a child command is running. |
ready | Liveness is satisfied, the one-time initial sync completed successfully, and the continuously syncing ob child is alive. This is the image's Docker healthcheck. |
The worker writes only its lifecycle phase, worker and child process IDs,
initial-sync completion, heartbeat timestamp, version, and revision to
/run/canterbury-sync/health.json. The file is mode 0600, is outside the
vault, and is replaced atomically. Health checks never invoke ob, read notes,
write the vault or audit log, or put credentials in command arguments.
Readiness does not prove that the current upstream WebSocket is connected after
startup. obsidian-headless does not expose a reliable ongoing connectivity
probe. A stalled Canterbury heartbeat or exited continuous child makes the
container unhealthy; transient upstream failures that the child handles
internally remain visible only in container logs.
On SIGTERM or SIGINT, the worker first reports not ready and forwards the
signal to ob. It allows 20 seconds for cleanup, then sends SIGKILL if the
upstream child has not exited. Compose grants the sync container 30 seconds to
stop. The MCP and vault services retain their 10-second application shutdown
deadlines and receive 15-second Compose stop grace periods.
All three services log non-sensitive build version and revision values at
startup. Image builds accept CANTERBURY_VERSION and CANTERBURY_REVISION
build arguments; local builds default to dev and unknown.
Run The Local Pomerium Stack
The default Docker Compose stack starts a local deployed-style auth path:
- Dex as a self-contained OIDC provider.
- Pomerium Core as the identity-aware gateway.
- The Canterbury vault service behind Pomerium.
- The internal Canterbury MCP server, routed through Pomerium at
/mcp.
Start it from the repository root:
CANTERBURY_UID="$(id -u)" CANTERBURY_GID="$(id -g)" docker compose up --build
Those CANTERBURY_UID and CANTERBURY_GID values make audit files in the
bind-mounted ./audit directory owned by your host user. When both values are
1000, Docker Compose's local defaults already match; setting them explicitly is
safer on systems that use different host IDs.
The protected vault route is available at:
https://vault.localhost.pomerium.io:8443
The MCP endpoint uses the same protected hostname:
https://vault.localhost.pomerium.io:8443/mcp
The stack uses local-only fixture keys, a self-signed certificate, and static Dex users. See Local Pomerium Stack for account details, manual testing steps, and the opt-in smoke test:
make smoke-pomerium
Run The Vault Service
The vault service reads from a local filesystem mirror of the vault. If you use
the default Docker-managed obsidian-vault volume, that volume is not exposed as
./vault on the host. For local service development, point
VAULT_SERVICE_ROOT at a host-accessible mirror, or deliberately use a bind
mount override such as ./vault:/vault after accounting for host filesystem
permissions.
Copy the vault service example environment file:
cp .env.example .env
Configure the vault service values:
VAULT_SERVICE_ROOT=./sample-vault
VAULT_SERVICE_AUTH_ISSUER=devauth.canterbury.local
VAULT_SERVICE_AUTH_AUDIENCE=canterbury.vault.local
VAULT_SERVICE_AUTH_JWKS_URL=http://127.0.0.1:50052/.well-known/jwks.json
VAULT_SERVICE_AUTH_MAPPING_FILE=./sample-auth/scopes.toml
VAULT_SERVICE_AUDIT_ROOT=./audit
VAULT_SERVICE_AUDIT_WRITER_ID=
VAULT_SERVICE_ADDR=127.0.0.1:50051
VAULT_SERVICE_HEALTHCHECK_URL=
VAULT_SERVICE_HEALTHCHECK_TIMEOUT=2s
VAULT_SERVICE_AUDIT_HMAC_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
| Variable | Required | Description |
|---|---|---|
VAULT_SERVICE_ROOT | Yes | Local filesystem path to the mirrored vault read by the Go vault service. |
VAULT_SERVICE_AUTH_ISSUER | Yes | Expected JWT issuer. |
VAULT_SERVICE_AUTH_AUDIENCE | Yes | Expected JWT audience. |
VAULT_SERVICE_AUTH_JWKS_URL | Yes | JWKS endpoint used to verify signed bearer JWTs. Its initial key set must load before the service becomes ready. |
VAULT_SERVICE_AUTH_MAPPING_FILE | Yes | TOML file mapping exact (issuer, subject) pairs to Canterbury scopes. |
VAULT_SERVICE_AUDIT_ROOT | Yes | Local filesystem directory where date-rotated JSONL audit logs are written outside the vault. |
VAULT_SERVICE_AUDIT_HMAC_KEY | Yes | Base64-encoded HMAC key, at least 32 decoded bytes, used to hash remote client addresses before writing audit records. |
VAULT_SERVICE_AUDIT_WRITER_ID | No | Optional filesystem-safe identifier included in audit filenames. When unset, the service generates one from hostname, PID, and a short random suffix. |
VAULT_SERVICE_ADDR | No | Address for the Connect server. Defaults to 127.0.0.1:50051 when not set. |
VAULT_SERVICE_HEALTHCHECK_URL | No | Complete Connect base URL for the healthcheck CLI. Defaults to http://<VAULT_SERVICE_ADDR>. |
VAULT_SERVICE_HEALTHCHECK_TIMEOUT | No | Positive timeout for the healthcheck CLI. Defaults to 2s. |
The .env.example file includes a development-only sample
VAULT_SERVICE_AUDIT_HMAC_KEY so the local demo starts without shell expansion
inside the dotenv file. Generate a real local or deployment key with:
openssl rand -base64 32
Keep this key stable if you need remote address hashes to remain comparable across service restarts. Rotate it when old audit address correlations should no longer be linkable.
Use ./sample-vault for a quick local demo. Point VAULT_SERVICE_ROOT at your
own host-accessible vault mirror when testing with real synced content.
The included sample vault contains a few fake notes for exercising the current read, search, tag, and authorization behavior:
| Path | Access scopes | Demo use |
|---|---|---|
Projects/Canterbury.md | personal-agent | Basic ReadNote and SearchNotes success. |
Projects/Agent Research.md | personal-agent | Text and tag search over project notes. |
Public/Service Brief.md | personal-agent, public-site | Multiple-scope access checks. |
Private/Unscoped Draft.md | None | Default-deny and permission-denied behavior. |
The Bruno collection in bruno/canterbury includes sample requests that target
the sample vault defaults. Add an Authorization: Bearer <token> header using a
token minted by the development auth service before sending vault requests.
The included sample-auth/scopes.toml maps the development auth subject
user_123 to personal-agent. Mint a local token with subject user_123 and
audience canterbury.vault.local when using the sample configuration.
The vault service loads .env from the repository root when present. Real
environment variables take precedence over values in .env.
Vault Readiness
The vault listener exposes the standard unauthenticated Connect/gRPC health
service. Readiness checks must request
canterbury.vault.v1.VaultService through grpc.health.v1.Health/Check.
The named service reports SERVING only after the vault root, independent
audit recorder, authorization mapping, and initial JWKS key set have loaded.
Initialization failures stop the process before it binds the listener, and the
status changes back to NOT_SERVING before graceful shutdown.
The included command provides the same bounded container-probe interface as the MCP service:
go run ./cmd/vault-service healthcheck
go run ./cmd/vault-service healthcheck --url http://vault-service:50051 --timeout 5s
It exits successfully only when the named vault service returns SERVING.
Configuration loads from .env, then the environment, and command flags take
precedence over both. The probe requires no bearer token, reads no notes, and
creates no audit events. It checks completed startup initialization rather than
continuously testing audit filesystem writes or later JWKS refreshes.
Both production Go images use the same exec-form Docker healthcheck cadence:
30-second intervals, a 5-second Docker timeout, a 5-second start period, and
three retries. Their application probe timeout defaults to 2 seconds. The MCP
image deliberately checks local operational health at /health/live and
excludes downstream outages; the vault image checks its own startup readiness
through Connect/gRPC. Neither image installs a general-purpose network toolbox
for probing. The image supplies its loopback URL through
MCP_SERVER_HEALTHCHECK_URL or VAULT_SERVICE_HEALTHCHECK_URL, so a deployment
that changes an internal listener can override the appropriate environment
variable without replacing the Docker healthcheck command.
Run The Local Auth Smoke Test
The auth smoke test is opt-in and is not part of make check. It starts both
local services, runs Bruno CLI assertions, and then tears the services down:
make smoke-auth
You can run the same flow through npm:
npm run smoke:auth
The runner starts go run ./cmd/dev-auth serve on 127.0.0.1:50052, waits for
/.well-known/jwks.json, starts go run ./cmd/vault-service on
127.0.0.1:50051, writes audit logs to a temporary directory, and runs
bruno/local-auth-smoke with the local environment. The smoke collection uses
Connect JSON requests so the Bruno CLI can mint tokens, chain runtime variables,
call the vault service, and assert success and failure cases in one ordered run.
For manual Bruno testing, start the services as shown below, then open or run the
collections under bruno/devauth, bruno/canterbury, or
bruno/local-auth-smoke. When using bruno/canterbury directly, mint a token
for subject user_123 and audience canterbury.vault.local, then set
VaultToken in the local environment before sending vault requests.
Start the development auth service first so the vault service can fetch its JWKS during startup:
go run ./cmd/dev-auth serve
In another shell, start the local vault service:
go run ./cmd/vault-service
The service exposes Connect/gRPC on the configured address. Vault RPCs require
an Authorization: Bearer <jwt> header. The currently implemented vault RPCs
are:
/canterbury.vault.v1.VaultService/ReadNote
/canterbury.vault.v1.VaultService/SearchNotes
Example ReadNote request body:
{
"ref": {
"path": "Projects/Canterbury.md"
}
}
ReadNote returns the Markdown body without frontmatter in note.content.
Parsed frontmatter is returned in note.metadata.properties after reserved
access-policy fields are removed. YAML timestamp values are formatted as RFC
3339 strings so they can be represented in google.protobuf.Struct.
Example SearchNotes request body:
{
"query": {
"text": "Canterbury"
},
"filter": {
"includePathPrefixes": ["Projects"],
"excludePathPrefixes": ["Projects/Archive"],
"allTags": ["project"],
"anyTags": ["ai", "notes"]
},
"pageSize": 25,
"sort": "SEARCH_SORT_PATH_ASC"
}
SearchNotes returns matching note references, metadata, snippets, and a
nextPageToken when another page is available. The current text query is a
single trimmed search term matched case-insensitively against note content. It
is not parsed as comma-separated syntax. Path prefix filters use vault-relative
paths. all_tags requires every listed tag, while any_tags requires at least
one listed tag when present. Supported sort orders are
SEARCH_SORT_PATH_ASC and SEARCH_SORT_MODIFIED_DESC.
Read and search attempts are written to
date-rotated JSONL audit logs under VAULT_SERVICE_AUDIT_ROOT; each service
process writes its own per-day file using VAULT_SERVICE_AUDIT_WRITER_ID or a
generated writer ID. If a required read or search audit record cannot be
written, the service fails instead of returning data. Authentication failures
also produce auth.failed audit events before the request is rejected. The
vault service should remain bound to a trusted internal interface.
The Connect vault handler attaches request-scoped audit metadata to each vault
RPC. It accepts or generates an X-Request-ID, returns that request ID on
success and error responses, extracts W3C traceparent trace IDs when present,
and records a keyed HMAC-SHA256 hash of the remote client address rather than
the raw address.
Run The MCP Server
Start the vault service first, then run the MCP gateway from the repository root:
go run ./cmd/mcp-server
The gateway serves stateless Streamable HTTP with JSON responses at
http://127.0.0.1:50053/mcp. Every request must contain exactly one
Authorization: Bearer <assertion> header. The gateway forwards that assertion,
X-Request-ID, and traceparent to the vault service and does not retain
identity-bearing MCP session state.
The same listener exposes two unauthenticated signals with deliberately different operational meanings:
| Endpoint | Contract |
|---|---|
/health/live | Reports local MCP operational health. A successful request proves configuration and handler initialization completed, the listener is answering, and shutdown has not begun. It does not contact the vault or reflect any other upstream outage. |
/health/ready | Reports dependency-inclusive readiness by combining local MCP health with the vault Connect/gRPC health status. Use it only when a caller specifically needs end-to-end vault availability, such as a deployment smoke test or an intentional downstream startup/traffic-admission gate. |
Both endpoints return only a gRPC-style status:
{ "status": "SERVING" }
SERVING returns HTTP 200. NOT_SERVING and UNKNOWN return HTTP 503.
Both endpoints support GET and HEAD, perform no vault reads or writes, and
do not create audit events. The dependency-inclusive readiness check is bounded
by MCP_SERVER_VAULT_REQUEST_TIMEOUT. During graceful shutdown, the local MCP
status changes to NOT_SERVING; readiness then short-circuits without
contacting the vault.
The health paths are intended only for internal deployment probes. They do not
require a bearer assertion and must not be routed publicly. The MCP image uses
/health/live for its Docker healthcheck so a vault or identity-provider outage
does not misclassify the locally functioning MCP process as broken and invite a
cascading restart policy. /health/ready remains available for consumers that
deliberately want dependency availability included in their decision. For
example:
curl --fail-with-body http://127.0.0.1:50053/health/live
curl --fail-with-body http://127.0.0.1:50053/health/ready
The included liveness command is useful for local scripts and container probes:
go run ./cmd/mcp-server healthcheck
go run ./cmd/mcp-server healthcheck --url http://mcp-server:50053/health/live --timeout 5s
It exits successfully only when its target returns SERVING. By default, it
checks http://<MCP_SERVER_ADDR>/health/live. Set a complete endpoint URL when
the probe must use a different listener, scheme, or path; the command does not
append a health path to that override. Configuration loads from .env, then
the environment, and command flags take precedence over both.
| Variable | Default | Purpose |
|---|---|---|
MCP_SERVER_ADDR | 127.0.0.1:50053 | MCP HTTP listen address. |
MCP_SERVER_VAULT_BASE_URL | http://127.0.0.1:50051 | Internal Connect vault service base URL. |
MCP_SERVER_VAULT_REQUEST_TIMEOUT | 10s | Positive timeout for each downstream RPC. |
MCP_SERVER_HEALTHCHECK_URL | Derived from server addr | Complete liveness endpoint for the CLI. |
MCP_SERVER_HEALTHCHECK_TIMEOUT | 2s | Positive timeout for the CLI operation. |
The public Compose deployment does not publish the MCP container port. Pomerium
routes only /mcp to the MCP server before the catch-all vault route; it does
not expose the /health/ subtree. Pomerium overwrites Authorization with its signed
assertion, and the gateway forwards that assertion unchanged. The shared route
hostname keeps Pomerium's JWT issuer and audience aligned with the vault service
checks. See
Pomerium JWT claim headers.
This deployment uses an ordinary protected HTTP route. It does not enable Pomerium's experimental MCP-native OAuth mode, so MCP clients must supply a bearer credential accepted by the existing Pomerium route.
Run The Development Auth Service
The development auth CLI is a local helper for JWT-based authentication testing.
It starts a Connect/gRPC service with health and reflection enabled on
127.0.0.1:50052:
go run ./cmd/dev-auth serve
The serve command accepts flags for its local server address and JWT issuer:
go run ./cmd/dev-auth serve \
--addr 127.0.0.1:50052 \
--issuer devauth.canterbury.local
| Variable | Flag | Description |
|---|---|---|
DEV_AUTH_ADDR | --addr | Address for the development auth Connect server. Defaults to 127.0.0.1:50052 when not set. |
DEV_AUTH_ISSUER | --issuer | Issuer claim placed in minted development JWTs. Defaults to devauth.canterbury.local. |
When .env is present at the repository root, the development auth command loads
these values from it without overriding real environment variables. Command-line
flags take precedence over environment and .env values. Run
go run ./cmd/dev-auth serve --help to print serve flags.
The MintToken RPC creates an EdDSA-signed bearer JWT for requested claims:
{
"claims": {
"subject": "user_123",
"audiences": ["canterbury.vault.local"]
},
"options": {
"ttlSeconds": 900
}
}
Omit options.ttlSeconds or set it to 0 to use the service default. The
application rejects missing subjects, missing audiences, negative TTLs, and TTLs
above the current local development maximum. The service exposes its public
verification key as JWKS at http://127.0.0.1:50052/.well-known/jwks.json so
local verifier work uses the same JWKS shape as deployed-style gateway auth.
The key is generated in memory when the service starts, so tokens minted before
a restart do not verify against the new endpoint response. The Bruno collection
in bruno/devauth points at the dev-auth default address.
Develop Canterbury
The recommended setup path installs repository dependencies and project-local Go tooling:
make setup
Run the full repository check through Make so the configured local toolchain is used:
make check
You can also run individual checks directly when you already have the required tools installed.
Install repository formatting dependencies:
npm install
Install Go linting tooling:
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b "$(go env GOPATH)/bin" v2.11.4
Install development dependencies for the sync component:
npm --prefix sync install
Run all npm-orchestrated checks:
npm run check
Format files from the repository root:
npm run format
Run Go tests:
npm run test:go
Run Go linting:
npm run lint:go
Lint sync worker files:
npm --prefix sync run lint
Auto-fix sync worker lint issues where possible:
npm --prefix sync run lint:fix
You can also run the sync component check directly:
npm --prefix sync run check
Troubleshoot
| Problem | Cause | Solution |
|---|---|---|
SYNC_VAULT_NAME is required | sync/.env is missing the vault name or ID. | Set SYNC_VAULT_NAME. |
SYNC_VAULT_PASSWORD is required | sync/.env is missing the vault encryption password. | Set SYNC_VAULT_PASSWORD. |
SYNC_OBSIDIAN_AUTH_TOKEN is required | sync/.env is missing the Obsidian auth token. | Set SYNC_OBSIDIAN_AUTH_TOKEN. |
required key VAULT_SERVICE_ROOT missing value | .env or the shell environment is missing the vault service root path. | Set VAULT_SERVICE_ROOT to ./sample-vault for the demo or to your mirrored vault path. |
required key VAULT_SERVICE_AUTH_ISSUER missing value | .env or the shell environment is missing the expected JWT issuer. | Set VAULT_SERVICE_AUTH_ISSUER to the issuer used by your auth provider or local dev-auth service. |
required key VAULT_SERVICE_AUTH_AUDIENCE missing value | .env or the shell environment is missing the expected JWT audience. | Set VAULT_SERVICE_AUTH_AUDIENCE, and mint/request tokens with the same audience. |
required key VAULT_SERVICE_AUTH_JWKS_URL missing value | .env or the shell environment is missing the JWKS URL. | Set VAULT_SERVICE_AUTH_JWKS_URL, such as http://127.0.0.1:50052/.well-known/jwks.json for local development. |
required key VAULT_SERVICE_AUTH_MAPPING_FILE missing value | .env or the shell environment is missing the scope mapping path. | Set VAULT_SERVICE_AUTH_MAPPING_FILE to a TOML mapping file such as ./sample-auth/scopes.toml. |
required key VAULT_SERVICE_AUDIT_ROOT missing value | .env or the shell environment is missing the audit log directory. | Set VAULT_SERVICE_AUDIT_ROOT to an explicit directory outside the vault, such as ./audit. |
required key VAULT_SERVICE_AUDIT_HMAC_KEY missing value | .env or the shell environment is missing the audit HMAC key. | Generate a literal key with openssl rand -base64 32 and set VAULT_SERVICE_AUDIT_HMAC_KEY. |
HMAC key must be base64 encoded | VAULT_SERVICE_AUDIT_HMAC_KEY is not base64. | Replace it with a literal key generated by openssl rand -base64 32. |
HMAC key must decode to at least 32 bytes | VAULT_SERVICE_AUDIT_HMAC_KEY decodes to fewer than 32 bytes. | Replace it with a literal key generated by openssl rand -base64 32. |
unauthenticated from a vault RPC | The request is missing a bearer token, the token is invalid, or the verified issuer/subject is not mapped. | Mint/request a fresh token with the configured issuer and audience, then add a matching (issuer, subject) entry to the auth mapping file if needed. |
permission denied; check your authorization scopes | The caller is authenticated, but the note does not declare a scope mapped to that subject. | Add a matching access.scopes value to the note or update the auth mapping file for local development. |
invalid search query | A search request contains an unsupported sort or invalid page token. | Use SEARCH_SORT_PATH_ASC or SEARCH_SORT_MODIFIED_DESC, and only reuse nextPageToken values returned by search. |
Another sync instance is already running for this vault. | Another sync process owns the vault lock, or the vault path is not writable. | Stop other sync clients for the same vault and confirm the container can write to /vault. |
| The container exits after a sync failure. | Compose is configured with restart: 'no'. | Inspect the logs with docker compose logs obsidian-sync, fix the configuration, then run docker compose up again. |
| Files are hard to inspect on the host. | The default Docker volume stores the vault outside the repository. | Use Docker volume tooling to inspect it, or deliberately configure a bind mount for local development. |
Roadmap
The planned MVP includes:
- Production identity-provider deployment guidance and more robust principal-to-scope policy management.
- Hardening and operationalizing vault sync through Obsidian Headless.
- Write-operation audit gating before any write paths are added.
- Basic indexing with SQLite.
Initial non-goals include:
- GraphQL APIs.
- Vector search.
- Complex permission inheritance.
- Autonomous write access.
- Multi-tenant support.
Contributing
Before contributing, run:
npm run check
Keep the default deployment portable. Prefer Docker-managed volumes for runtime storage, and treat host bind mounts as local development overrides.
Additional Documentation
obsidian-headless- Obsidian Sync
- Docker Compose
- Canterbury Architecture
- Auth V1 Design
- Audit System Design
- Local Pomerium Stack
- Dependency Maintenance
- The Good Docs Project README template guide
How To Get Help
Start by checking the sync worker logs:
docker compose logs obsidian-sync
For problems with Obsidian account access, vault encryption, or Sync service behavior, consult Obsidian support resources. For problems with Canterbury, open an issue or discussion in the repository where this project is hosted.
Terms Of Use
This project is licensed as UNLICENSED in sync/package.json.
Obsidian, Obsidian Sync, and obsidian-headless are separate projects with their
own terms and licenses.