Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
58eec1c
deps: add platform-specific dependencies for OS service management
krishna-santosh Jun 25, 2026
8366b60
refactor(grpc): add shutdown future parameter for graceful drain
krishna-santosh Jun 25, 2026
12cc20c
refactor: extract server bootstrap into reusable server module
krishna-santosh Jun 25, 2026
6c79f6b
feat: add cross-platform OS service management
krishna-santosh Jun 25, 2026
9888fbe
refactor(paths): support platform-conventional split layout with sing…
krishna-santosh Jun 30, 2026
eda22ef
feat(service): pin OS service homes to system-owned directories
krishna-santosh Jun 30, 2026
aaa373d
refactor(mcp): rewrite stdio transport as thin HTTP proxy
krishna-santosh Jun 30, 2026
ffd3e2f
docs: document split layout, MCP stdio proxy, and OS service management
krishna-santosh Jun 30, 2026
bb52640
chore(deps): update dependencies
krishna-santosh Jul 1, 2026
7dc2c53
refactor(mcp): replace Content with ContentBlock and clean up imports…
krishna-santosh Jul 1, 2026
46e6bf5
refactor(crypto): update AES-GCM nonce and key handling for improved …
krishna-santosh Jul 1, 2026
4c452fb
refactor(paths): extract system_home(), split layout resolution into …
krishna-santosh Jul 1, 2026
217ff3d
fix(service/windows): harden install/uninstall lifecycle and use shar…
krishna-santosh Jul 1, 2026
28a146a
refactor(service/linux): use shared paths::system_home() instead of l…
krishna-santosh Jul 1, 2026
e4a82f9
refactor(service/macos): use shared paths::system_home() instead of l…
krishna-santosh Jul 1, 2026
c42891d
fix(main): tolerate PermissionDenied when loading .env from system home
krishna-santosh Jul 1, 2026
7a12b03
docs(AGENTS.md): clarify single-layout precedence and elevation requi…
krishna-santosh Jul 1, 2026
ff13003
docs(CLAUDE.md): clarify single-layout precedence and elevation requi…
krishna-santosh Jul 1, 2026
bd9c4bb
docs(README): explain that CLI follows the service's data directory
krishna-santosh Jul 1, 2026
a447f13
chore(deps): add futures-util and infer; promote tokio-stream to main…
krishna-santosh Jul 2, 2026
bb2adcb
feat(upload): add magic bytes content sniffing for MIME validation
krishna-santosh Jul 2, 2026
ebe21a3
feat(storage): add multipart streaming upload to StorageProvider
krishna-santosh Jul 2, 2026
53fa44e
feat(repo): add set_thumbnail_meta, unique-violation guard, and last_…
krishna-santosh Jul 2, 2026
6f6e777
feat(upload): rewrite file service for constant-memory streaming
krishna-santosh Jul 2, 2026
31a92ea
feat(upload): add signed URL PUT endpoint with configurable expiry
krishna-santosh Jul 2, 2026
adceb99
feat(mcp): improve create_upload_url with content validation
krishna-santosh Jul 2, 2026
92a6fee
perf(auth): debounce session/token last-sSeen timestamp updates
krishna-santosh Jul 2, 2026
9463bdf
perf(search): make indexer purely event-driven, remove polling
krishna-santosh Jul 2, 2026
77591e0
feat(dashboard): add ETag and cache-control headers for embedded assets
krishna-santosh Jul 2, 2026
1773b57
fix(server): pre-bind TCP listeners before declaring readiness
krishna-santosh Jul 2, 2026
72770bd
fix(windows): improve service lifecycle, error reporting, and ACL har…
krishna-santosh Jul 2, 2026
f1034af
fix(macos): poll for process exit on service stop
krishna-santosh Jul 2, 2026
949658e
perf(repo): wrap collection migration in transaction; batch file refs
krishna-santosh Jul 2, 2026
95fd2b0
perf(backup): offload CPU-bound compression and encryption to blockin…
krishna-santosh Jul 2, 2026
c280122
docs: update agent instructions for streaming uploads and search
krishna-santosh Jul 2, 2026
f40e42e
test: add integration tests for streaming uploads, sniffing, and sign…
krishna-santosh Jul 2, 2026
59b68e0
refactor: simplify sudo user validation logic in resolve_run_user fun…
krishna-santosh Jul 3, 2026
044c237
docs: clarify VCMS_HOME resolution order in README
krishna-santosh Jul 3, 2026
db11d7d
docs(cli): clarify Windows service --user flag behavior
krishna-santosh Jul 3, 2026
3c06420
fix(config): validate upload_token_expiry_secs is positive and use co…
krishna-santosh Jul 3, 2026
4e14df8
fix(mysql): pin session timezone to UTC via after_connect
krishna-santosh Jul 3, 2026
d7ee538
test(dashboard): add unit tests for cache-control headers and ETag 304
krishna-santosh Jul 3, 2026
ea6c342
fix(server): await both servers on graceful shutdown and handle admin…
krishna-santosh Jul 3, 2026
3138ca7
fix(search): add retry with exponential backoff to indexer drain
krishna-santosh Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 59 additions & 22 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,22 @@ vcms admin reset-password --email U --password P
vcms backup create [--scope instance|site] [--site ID] [--out FILE] [--no-files] [--encrypt]
vcms backup list # list recorded backups
vcms restore --file PATH [--scope instance|site] [--site ID] [--import-as-new] --yes
vcms service install [--user NAME] # install/enable/start as an OS service (systemd/launchd/SCM)
vcms service uninstall|status|start|stop
vcms mcp stdio # thin HTTP proxy to a running server's /mcp (for MCP clients)
```

`backup`/`restore` run offline (no HTTP server) against the configured database —
the disaster-recovery path when the instance won't boot. `restore` is destructive
and requires `--yes`.

`mcp stdio` opens no database, secrets, or search index of its own: it forwards
JSON-RPC between stdin/stdout and the server's `/mcp` Streamable-HTTP endpoint,
reading only `VCMS_MCP_TOKEN` (bearer) and `VCMS_MCP_URL` (default
`http://127.0.0.1:3000`). So it works even when the data is owned by the OS-service
account. `vcms service install` pins `VCMS_HOME` to a system dir so the daemon stores
everything under one owned root.

Global flags (highest precedence): `--config <PATH>`, `--bind <ADDR>`, `--database-url <URL>`, `--log-level <LEVEL>`.

The server auto-migrates the database on every startup; there is no separate migrate command.
Expand All @@ -93,29 +103,53 @@ Layers merge with precedence: **CLI flag > env var > config file > built-in defa
Config file search order (first existing wins; missing is fine):
1. `--config` flag / `VCMS_CONFIG` env
2. `./vcms.toml` (current dir)
3. `~/.vcms/config.toml` (CMS home; `$VCMS_HOME/config.toml` if set) — where `vcms config init` writes
3. the platform config dir (`config.toml`; `$VCMS_HOME/config.toml` in single-dir mode) — where `vcms config init` writes
4. `/etc/vcms/config.toml`

## Data directory (CMS home)
## Data directory

Resolution lives in `apps/backend/src/paths.rs` and has two layouts:

**Split (default, interactive installs)** — files land in the platform-conventional
per-type directories via the `directories` crate (`ProjectDirs`):

| File(s) | Dir | Linux | macOS | Windows |
|---------|-----|-------|-------|---------|
| `config.toml`, `secrets.toml`, `.env` | config | `~/.config/vcms` | `~/Library/Application Support/vcms` | `%APPDATA%\vcms\config` |
| `vcms.db`, `storage/`, `backups/` | data | `~/.local/share/vcms` | `~/Library/Application Support/vcms` | `%APPDATA%\vcms\data` |
| `search/` (derived, rebuildable) | cache | `~/.cache/vcms` | `~/Library/Caches/vcms` | `%LOCALAPPDATA%\vcms\cache` |
| `logs/` | state | `~/.local/state/vcms` | `~/Library/Application Support/vcms` | `%LOCALAPPDATA%\vcms\data` |

All runtime files live under one home directory: `$VCMS_HOME` if set, else `~/.vcms`
(same layout on Windows, macOS, Linux via the `directories` crate). `vcms serve`
creates it on first run. Resolution lives in `apps/backend/src/paths.rs`.
(`logs/` uses the state dir where the platform has one — Linux — else the local data dir.)

**Single** — everything nests under one root. Chosen (in precedence order) when:
1. **`$VCMS_HOME` is set** — forces the root explicitly.
2. **the system service home dir exists** — Linux `/var/lib/vcms`, macOS
`/Library/Application Support/vcms`, Windows `C:\ProgramData\vcms`. The
`vcms service` installer creates it (and leaves it behind on uninstall), so a plain
`vcms serve`/`admin`/`backup` **follows the service's data instead of forking to a
per-user split store**. This path is defined once in `paths::system_home()` and
imported by the `service` submodules.
3. **a legacy `~/.vcms` exists** — an existing install keeps working untouched.

Otherwise (dev/eval boxes with no service) files use the platform split dirs.

```text
~/.vcms/
config.toml # non-secret config (vcms config init target)
secrets.toml # auto-generated HMAC_SECRET + backup key (0600 on unix)
vcms.db # default SQLite database (+ -wal / -shm)
logs/ # rolling logs when [log] output = "file"
storage/ # default filesystem storage for uploads
$VCMS_HOME/ # or system home, or ~/.vcms (legacy)
config.toml secrets.toml .env
vcms.db (+ -wal / -shm) logs/ storage/ backups/ search/
```

Secrets: on first `serve`/`admin`, a random `HMAC_SECRET` is generated
and persisted to `secrets.toml` (`apps/backend/src/secrets.rs`), then loaded by
every process — including `vcms mcp stdio`, which is launched from an arbitrary cwd
and so cannot rely on a cwd `.env`. Env vars still override the file. `mcp stdio`
is read-only: it never creates the home dir, database, or secrets file.
When the active home is the system service home (owned by SYSTEM/root), the
data-touching commands (`serve`/`admin`/`backup`/`restore`) require elevation — a
non-elevated invocation **fails fast** with an "Administrator/root" hint (in
`paths::ensure`'s preflight) rather than silently forking to a second store.

Secrets: on first `serve`/`admin`, a random `HMAC_SECRET` is generated and persisted to
`secrets.toml` (`apps/backend/src/secrets.rs`), then loaded by the server processes.
`vcms mcp stdio` does **not** load secrets, the database, or any data-dir file: it is a
thin HTTP proxy to the running server's `/mcp` (see CLI), forwarding `VCMS_MCP_TOKEN` as
the bearer; the server owns all disk I/O.

Env-only secrets (never read from `config.toml` by convention, omitted from
`config init`): `DATABASE_URL`, `HMAC_SECRET`, `S3_ACCESS_KEY_ID`,
Expand Down Expand Up @@ -153,8 +187,10 @@ Logging keys map to the `[log]` table: `RUST_LOG`→`log.level`, `LOG_OUTPUT`→
| Variable | Default | Description |
|----------|---------|-------------|
| `VCMS_CONFIG` | - | Explicit config file path (same as `--config`) |
| `VCMS_HOME` | `~/.vcms` | CMS home directory (db, config, secrets, logs, storage) |
| `DATABASE_URL` | `sqlite://~/.vcms/vcms.db` | Database URL: `sqlite:path`, `postgres://...`, `mysql://...` |
| `VCMS_HOME` | - | If set, forces single-dir mode: db/config/secrets/logs/storage all nest under this root (else files use the platform split dirs) |
| `VCMS_MCP_TOKEN` | - | `vcms_site_*` access token forwarded by `vcms mcp stdio` as the bearer credential (required for stdio) |
| `VCMS_MCP_URL` | `http://127.0.0.1:3000` | Running server's base URL that `vcms mcp stdio` proxies to (`{url}/mcp`) |
| `DATABASE_URL` | `sqlite://<data dir>/vcms.db` | Database URL: `sqlite:path`, `postgres://...`, `mysql://...` |
| `HMAC_SECRET` | auto | HMAC key for token lookup (required; auto-generated to `secrets.toml`, env overrides) |
| `BIND_ADDRESS` | `0.0.0.0:3000` | REST API listen address |
| `GRPC_BIND_ADDRESS` | `0.0.0.0:50051` | gRPC server listen address |
Expand All @@ -167,14 +203,15 @@ Logging keys map to the `[log]` table: `RUST_LOG`→`log.level`, `LOG_OUTPUT`→
| `S3_PUBLIC_URL` | - | Public URL for S3 assets |
| `BACKUP_ENABLED` | `true` | Run the scheduled-backup poller / allow backups |
| `BACKUP_DESTINATION` | `filesystem` | Backup destination: `filesystem` or `s3` |
| `BACKUP_LOCAL_PATH` | `~/.vcms/backups` | Local backup dir (when destination is filesystem) |
| `BACKUP_LOCAL_PATH` | `<data dir>/backups` | Local backup dir (when destination is filesystem) |
| `BACKUP_ZSTD_LEVEL` | `12` | zstd compression level for backups |
| `BACKUP_DEFAULT_RETENTION` | `7` | Default "keep last N" for new schedules |
| `BACKUP_S3_BUCKET` / `_REGION` / `_ENDPOINT` / `_PUBLIC_URL` | - | S3 backup destination (non-secret parts) |
| `BACKUP_S3_ACCESS_KEY_ID` | - | S3 backup access key (secret, env-only) |
| `BACKUP_S3_SECRET_ACCESS_KEY` | - | S3 backup secret key (secret, env-only) |
| `BACKUP_ENCRYPTION_KEY` | auto | AES-256 backup key (hex); auto-generated to `secrets.toml` |
| `MAX_UPLOAD_SIZE_MB` | `50` | Max upload size in MB |
| `UPLOAD_TOKEN_EXPIRY_SECS` | `900` | Signed upload URL lifetime (seconds) |
| `COOKIE_SECURE` | `false` | Require HTTPS cookies |
| `DB_MAX_CONNECTIONS` | `10` | Max DB connections |
| `DB_MIN_CONNECTIONS` | `2` | Min DB connections |
Expand All @@ -185,10 +222,10 @@ Logging keys map to the `[log]` table: `RUST_LOG`→`log.level`, `LOG_OUTPUT`→
| `LOG_OUTPUT` | `stdout` | `stdout` or `file` (`[log] output`) |
| `LOG_FORMAT` | `pretty` | `pretty` or `json` (`[log] format`) |
| `LOG_ANNOTATIONS` | `false` | Include file + line numbers (`[log] annotations`) |
| `LOG_DIR` | `~/.vcms/logs` | Log directory when `output = file` (`[log] dir`) |
| `LOG_DIR` | `<state dir>/logs` | Log directory when `output = file` (`[log] dir`) |

**Note**: `HMAC_SECRET` is auto-generated and persisted to
`~/.vcms/secrets.toml` on first run. Set it explicitly via env to override.
**Note**: `HMAC_SECRET` is auto-generated and persisted to the config dir's
`secrets.toml` on first run. Set it explicitly via env to override.

## Proto Compilation

Expand Down
95 changes: 63 additions & 32 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,30 +94,54 @@ Layers merge with precedence: **CLI flag > env var > config file > built-in defa
Config file search order (first existing wins; missing is fine):
1. `--config` flag / `VCMS_CONFIG` env
2. `./vcms.toml` (current dir)
3. `~/.vcms/config.toml` (CMS home; `$VCMS_HOME/config.toml` if set) — where `vcms config init` writes
3. the platform config dir (`config.toml`; `$VCMS_HOME/config.toml` in single-dir mode) — where `vcms config init` writes
4. `/etc/vcms/config.toml`

## Data directory (CMS home)
## Data directory

All runtime files live under one home directory: `$VCMS_HOME` if set, else `~/.vcms`
(same layout on Windows, macOS, Linux via the `directories` crate). `vcms serve`
creates it on first run. Resolution lives in `apps/backend/src/paths.rs`.
Resolution lives in `apps/backend/src/paths.rs` and has two layouts:

**Split (default, interactive installs)** — files land in the platform-conventional
per-type directories via the `directories` crate (`ProjectDirs`):

| File(s) | Dir | Linux | macOS | Windows |
|---------|-----|-------|-------|---------|
| `config.toml`, `secrets.toml`, `.env` | config | `~/.config/vcms` | `~/Library/Application Support/vcms` | `%APPDATA%\vcms\config` |
| `vcms.db`, `storage/`, `backups/` | data | `~/.local/share/vcms` | `~/Library/Application Support/vcms` | `%APPDATA%\vcms\data` |
| `search/` (derived, rebuildable) | cache | `~/.cache/vcms` | `~/Library/Caches/vcms` | `%LOCALAPPDATA%\vcms\cache` |
| `logs/` | state | `~/.local/state/vcms` | `~/Library/Application Support/vcms` | `%LOCALAPPDATA%\vcms\data` |

(`logs/` uses the state dir where the platform has one — Linux — else the local data dir.)

**Single** — everything nests under one root. Chosen (in precedence order) when:
1. **`$VCMS_HOME` is set** — forces the root explicitly.
2. **the system service home dir exists** — Linux `/var/lib/vcms`, macOS
`/Library/Application Support/vcms`, Windows `C:\ProgramData\vcms`. The
`vcms service` installer creates it (and leaves it behind on uninstall), so a plain
`vcms serve`/`admin`/`backup` **follows the service's data instead of forking to a
per-user split store**. This path is defined once in `paths::system_home()` and
imported by the `service` submodules.
3. **a legacy `~/.vcms` exists** — an existing install keeps working untouched.

Otherwise (dev/eval boxes with no service) files use the platform split dirs.

```text
~/.vcms/
config.toml # non-secret config (vcms config init target)
secrets.toml # auto-generated HMAC_SECRET + backup key (0600 on unix)
vcms.db # default SQLite database (+ -wal / -shm)
logs/ # rolling logs when [log] output = "file"
storage/ # default filesystem storage for uploads
search/ # Tantivy full-text search index (derived; rebuildable)
$VCMS_HOME/ # or system home, or ~/.vcms (legacy)
config.toml secrets.toml .env
vcms.db (+ -wal / -shm) logs/ storage/ backups/ search/
```

`vcms serve`/`admin` create the dirs they need on first run; `vcms mcp stdio` creates
nothing. When the active home is the system service home (owned by SYSTEM/root), the
data-touching commands (`serve`/`admin`/`backup`/`restore`) require elevation — a
non-elevated invocation **fails fast** with an "Administrator/root" hint (in
`paths::ensure`'s preflight) rather than silently forking to a second store.

Secrets: on first `serve`/`admin`, a random `HMAC_SECRET` is generated
and persisted to `secrets.toml` (`apps/backend/src/secrets.rs`), then loaded by
every process — including `vcms mcp stdio`, which is launched from an arbitrary cwd
and so cannot rely on a cwd `.env`. Env vars still override the file. `mcp stdio`
is read-only: it never creates the home dir, database, or secrets file.
the server processes. `vcms mcp stdio` does **not** load secrets, the database, or
any home-dir file: it is a thin HTTP proxy (see below) and the server it forwards to
owns all of those.

Env-only secrets (never read from `config.toml` by convention, omitted from
`config init`): `DATABASE_URL`, `HMAC_SECRET`, `S3_ACCESS_KEY_ID`,
Expand Down Expand Up @@ -156,8 +180,10 @@ Logging keys map to the `[log]` table: `RUST_LOG`→`log.level`, `LOG_OUTPUT`→
| Variable | Default | Description |
|----------|---------|-------------|
| `VCMS_CONFIG` | - | Explicit config file path (same as `--config`) |
| `VCMS_HOME` | `~/.vcms` | CMS home directory (db, config, secrets, logs, storage) |
| `DATABASE_URL` | `sqlite://~/.vcms/vcms.db` | Database URL: `sqlite:path`, `postgres://...`, `mysql://...` |
| `VCMS_HOME` | - | If set, forces single-dir mode: db/config/secrets/logs/storage all nest under this root (else files use the platform split dirs) |
| `VCMS_MCP_TOKEN` | - | `vcms_site_*` access token forwarded by `vcms mcp stdio` as the bearer credential (required for stdio) |
| `VCMS_MCP_URL` | `http://127.0.0.1:3000` | Running server's base URL that `vcms mcp stdio` proxies to (`{url}/mcp`) |
| `DATABASE_URL` | `sqlite://<data dir>/vcms.db` | Database URL: `sqlite:path`, `postgres://...`, `mysql://...` |
| `HMAC_SECRET` | auto | HMAC key for token lookup (required; auto-generated to `secrets.toml`, env overrides) |
| `BIND_ADDRESS` | `0.0.0.0:3000` | REST API listen address |
| `GRPC_BIND_ADDRESS` | `0.0.0.0:50051` | gRPC server listen address |
Expand All @@ -170,16 +196,17 @@ Logging keys map to the `[log]` table: `RUST_LOG`→`log.level`, `LOG_OUTPUT`→
| `S3_PUBLIC_URL` | - | Public URL for S3 assets |
| `BACKUP_ENABLED` | `true` | Run the scheduled-backup poller / allow backups |
| `BACKUP_DESTINATION` | `filesystem` | Backup destination: `filesystem` or `s3` |
| `BACKUP_LOCAL_PATH` | `~/.vcms/backups` | Local backup dir (when destination is filesystem) |
| `BACKUP_LOCAL_PATH` | `<data dir>/backups` | Local backup dir (when destination is filesystem) |
| `BACKUP_ZSTD_LEVEL` | `12` | zstd compression level for backups |
| `BACKUP_DEFAULT_RETENTION` | `7` | Default "keep last N" for new schedules |
| `BACKUP_S3_BUCKET` / `_REGION` / `_ENDPOINT` / `_PUBLIC_URL` | - | S3 backup destination (non-secret parts) |
| `BACKUP_S3_ACCESS_KEY_ID` | - | S3 backup access key (secret, env-only) |
| `BACKUP_S3_SECRET_ACCESS_KEY` | - | S3 backup secret key (secret, env-only) |
| `BACKUP_ENCRYPTION_KEY` | auto | AES-256 backup key (hex); auto-generated to `secrets.toml` |
| `SEARCH_ENABLED` | `true` | Build/use the Tantivy full-text index for entry search (else SQL `LIKE`) |
| `SEARCH_INDEX_PATH` | `~/.vcms/search` | Directory for the Tantivy search index |
| `SEARCH_INDEX_PATH` | `<cache dir>/search` | Directory for the Tantivy search index |
| `MAX_UPLOAD_SIZE_MB` | `50` | Max upload size in MB |
| `UPLOAD_TOKEN_EXPIRY_SECS` | `900` | Signed upload URL lifetime (seconds) |
| `COOKIE_SECURE` | `false` | Require HTTPS cookies |
| `DB_MAX_CONNECTIONS` | `10` | Max DB connections |
| `DB_MIN_CONNECTIONS` | `2` | Min DB connections |
Expand All @@ -190,10 +217,10 @@ Logging keys map to the `[log]` table: `RUST_LOG`→`log.level`, `LOG_OUTPUT`→
| `LOG_OUTPUT` | `stdout` | `stdout` or `file` (`[log] output`) |
| `LOG_FORMAT` | `pretty` | `pretty` or `json` (`[log] format`) |
| `LOG_ANNOTATIONS` | `false` | Include file + line numbers (`[log] annotations`) |
| `LOG_DIR` | `~/.vcms/logs` | Log directory when `output = file` (`[log] dir`) |
| `LOG_DIR` | `<state dir>/logs` | Log directory when `output = file` (`[log] dir`) |

**Note**: `HMAC_SECRET` is auto-generated and persisted to
`~/.vcms/secrets.toml` on first run. Set it explicitly via env to override.
**Note**: `HMAC_SECRET` is auto-generated and persisted to the config dir's
`secrets.toml` on first run. Set it explicitly via env to override.

## Proto Compilation

Expand Down Expand Up @@ -298,23 +325,28 @@ is **derived** and fully rebuildable.
scalar text of `data`, English-stemmed, BM25-ranked). `fields_from` re-resolves the
schema when opening an existing index read-only.
- `mod.rs` — `SearchService`. **Reading** needs no lock: `open_read_only` (reader
only) lets *any* process search the index, including a separate `vcms mcp stdio`
running next to the server. **Writing** requires the directory lock: `open`
only) lets *any* auxiliary process search the index alongside the server (this is a
general capability; `vcms mcp stdio` itself no longer opens the index — it proxies
over HTTP). **Writing** requires the directory lock: `open`
(reader + writer) is held only by the running server. `index_doc`/`delete_doc`
stage uncommitted ops; `commit` flushes; `rebuild_all`/`rebuild_site` reindex from
the DB by reusing `EntryRepository::list` (covers singletons too). Write/commit on
a read-only instance returns `SearchError::ReadOnly`.
- `queue.rs` — `SearchQueue` over the `search_index_queue` table (migration
`20260618000000`). Content writes from *any* process enqueue here
`20260618000000`). Content writes enqueue here
(`enqueue`/`dequeue_batch`/`delete_ids`); UUIDv7 ids order the queue chronologically.
The table exists purely for **durability** (crash recovery), not cross-process
signaling — all producers run in the server process (`vcms mcp stdio` is an HTTP proxy).
- `indexer.rs` — the **single consumer**, spawned once by the server (it owns the
writer). Drains the queue in batches (present in DB ⇒ upsert doc, absent ⇒ delete
doc — `op` is advisory), commits per batch, deletes processed rows. Wakes instantly
on a local enqueue (`Notify`) and polls every 2s to catch other processes' enqueues.
doc — `op` is advisory), commits per batch, deletes processed rows. **Purely
event-driven**: one startup drain (rows left by a crash), then sleeps until an
enqueue rings the in-process `Notify` — no polling.

Wiring: `Services` holds `search: Option<Arc<SearchService>>` (reads) and
`search_queue: Option<Arc<SearchQueue>>` (writes). `Services::new` opens the index
read-write (server); `Services::new_read_only` opens it read-only (`vcms mcp stdio`).
read-write (server); `Services::new_read_only` opens it read-only for an auxiliary
process that searches without taking the writer lock.
`EntryService`/`SingletonService` **enqueue** on write; `EntryService::list_entries`
queries the index — so REST, GraphQL, gRPC, and MCP all get ranked search via the
existing `search` param with **no handler changes**. Indexing is asynchronous
Expand All @@ -324,10 +356,9 @@ index builds on startup when empty, rebuilds after a restore, and exposes
owner/operator reindex routes: `POST /api/dashboard/instance/search/reindex` and
`POST /api/dashboard/sites/{site_id}/search/reindex`.

This is the cross-process model: writes from `vcms mcp stdio` (or any process) land in
the durable queue and the running server indexes them; if the server is down they
drain on its next start. The remaining hard limit is *concurrent writers* — only one
process indexes at a time.
Writes land in the durable queue and the running server indexes them; rows enqueued
before a crash drain on the next start. The remaining hard limit is *concurrent
writers* — only one process indexes at a time.

### No typo tolerance (deliberate) — and how to add it

Expand Down
Loading
Loading