Skip to content
Conduit

mcp server

canterbury

An experimental system for connecting AI agents to an Obsidian vault through a controlled service layer.

cthierer/canterburycommunitylistedunreviewed

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, 2026

Canterbury

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-headless integration.
  • 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, and SearchNotes handlers.
  • A stateless Streamable HTTP MCP gateway exposing the allowlisted read_note and search_notes tools at POST /mcp, with internal liveness and readiness endpoints at GET /health/live and GET /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-lint 2.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

  1. Clone this repository.
  2. Generate local Pomerium config, certificates, and secrets:
scripts/setup-local-pomerium.mts
  1. 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
VariableRequiredDescription
SYNC_VAULT_NAMEYesRemote Obsidian vault name or ID.
SYNC_VAULT_PASSWORDYesEnd-to-end encryption password for the remote vault.
SYNC_OBSIDIAN_AUTH_TOKENYesObsidian auth token used by obsidian-headless.
SYNC_DEVICE_NAMENoDevice name shown in Obsidian Sync history. Defaults to canterbury-sync.
SYNC_VAULT_PATHNoLocal 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.

ModeContract
liveThe supervisor heartbeat is fresh, shutdown has not begun, and the active ob child is alive when a child command is running.
readyLiveness 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=
VariableRequiredDescription
VAULT_SERVICE_ROOTYesLocal filesystem path to the mirrored vault read by the Go vault service.
VAULT_SERVICE_AUTH_ISSUERYesExpected JWT issuer.
VAULT_SERVICE_AUTH_AUDIENCEYesExpected JWT audience.
VAULT_SERVICE_AUTH_JWKS_URLYesJWKS endpoint used to verify signed bearer JWTs. Its initial key set must load before the service becomes ready.
VAULT_SERVICE_AUTH_MAPPING_FILEYesTOML file mapping exact (issuer, subject) pairs to Canterbury scopes.
VAULT_SERVICE_AUDIT_ROOTYesLocal filesystem directory where date-rotated JSONL audit logs are written outside the vault.
VAULT_SERVICE_AUDIT_HMAC_KEYYesBase64-encoded HMAC key, at least 32 decoded bytes, used to hash remote client addresses before writing audit records.
VAULT_SERVICE_AUDIT_WRITER_IDNoOptional filesystem-safe identifier included in audit filenames. When unset, the service generates one from hostname, PID, and a short random suffix.
VAULT_SERVICE_ADDRNoAddress for the Connect server. Defaults to 127.0.0.1:50051 when not set.
VAULT_SERVICE_HEALTHCHECK_URLNoComplete Connect base URL for the healthcheck CLI. Defaults to http://<VAULT_SERVICE_ADDR>.
VAULT_SERVICE_HEALTHCHECK_TIMEOUTNoPositive 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:

PathAccess scopesDemo use
Projects/Canterbury.mdpersonal-agentBasic ReadNote and SearchNotes success.
Projects/Agent Research.mdpersonal-agentText and tag search over project notes.
Public/Service Brief.mdpersonal-agent, public-siteMultiple-scope access checks.
Private/Unscoped Draft.mdNoneDefault-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:

EndpointContract
/health/liveReports 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/readyReports 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.

VariableDefaultPurpose
MCP_SERVER_ADDR127.0.0.1:50053MCP HTTP listen address.
MCP_SERVER_VAULT_BASE_URLhttp://127.0.0.1:50051Internal Connect vault service base URL.
MCP_SERVER_VAULT_REQUEST_TIMEOUT10sPositive timeout for each downstream RPC.
MCP_SERVER_HEALTHCHECK_URLDerived from server addrComplete liveness endpoint for the CLI.
MCP_SERVER_HEALTHCHECK_TIMEOUT2sPositive 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
VariableFlagDescription
DEV_AUTH_ADDR--addrAddress for the development auth Connect server. Defaults to 127.0.0.1:50052 when not set.
DEV_AUTH_ISSUER--issuerIssuer 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

ProblemCauseSolution
SYNC_VAULT_NAME is requiredsync/.env is missing the vault name or ID.Set SYNC_VAULT_NAME.
SYNC_VAULT_PASSWORD is requiredsync/.env is missing the vault encryption password.Set SYNC_VAULT_PASSWORD.
SYNC_OBSIDIAN_AUTH_TOKEN is requiredsync/.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 encodedVAULT_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 bytesVAULT_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 RPCThe 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 scopesThe 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 queryA 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

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.

↑↓ navigate openesc close