- Add k8s/ manifests (StatefulSet, kustomize, PDB, ServiceMonitor) + docs/runbooks/kubernetes.md - Add tidal-server/src/openapi.rs (utoipa OpenAPI spec) and wire into router - Add docs/guides/ (build-a-feed-app, embeddings, server-deployment) + foryou_feed example - Consolidate tidal/docker/ into root docker/ (single canonical home) - Update API.md, QUICKSTART.md, README.md, CLAUDE.md, check-docs.sh accordingly
445 lines
20 KiB
Markdown
445 lines
20 KiB
Markdown
# tidalDB Cluster Runbook
|
||
|
||
Operating the multi-region `tidal-server cluster` surface: launch, the
|
||
operational API, replication transport facts, failover and partition drills,
|
||
and the honest write-durability contract.
|
||
|
||
> ## STATUS: EXPERIMENTAL — SINGLE-PROCESS, NOT PRODUCTION HA
|
||
>
|
||
> Cluster mode runs **every region inside one `tidal-server` process**. The
|
||
> regions replicate over the **real `tidal-net` gRPC transport** (`ShipSegment`
|
||
> RPC, serialization, circuit breaker, HTTP/2 — see [§4](#4-grpc-replication-transport-tidal-net)),
|
||
> but that transport is a **loopback self-loop**: a follower's gRPC server and
|
||
> its only peer entry are the same loopback address. So you get faithful
|
||
> multi-region replication *semantics* over a real wire, with **no host or
|
||
> process isolation**.
|
||
>
|
||
> **A process crash, OOM, or host failure takes the entire "cluster" down at
|
||
> once.** There is no second machine to fail over to. This is a development /
|
||
> staging / demo fabric and a correctness harness for the replication paths —
|
||
> **it is NOT production high availability.**
|
||
>
|
||
> True multi-process, multi-host HA (process isolation, real network between
|
||
> regions, quorum-acked writes) is tracked as **m8p10** in
|
||
> [docs/planning/ROADMAP.md](../planning/ROADMAP.md). Until then, for a real
|
||
> production deployment run a **single `tidal-server standalone`** node and back
|
||
> it with host-level redundancy and disk durability.
|
||
>
|
||
> **Cluster mode refuses to start** unless you explicitly opt in with either the
|
||
> `--experimental-cluster` flag or the `TIDAL_ALLOW_EXPERIMENTAL_CLUSTER=1`
|
||
> environment variable. On start it emits a loud `WARN` restating exactly the
|
||
> above. Do not wire it into a production load balancer.
|
||
|
||
## Scope: what cluster mode does and does not own
|
||
|
||
tidalDB owns **retrieval and ranking only**. Cluster mode adds multi-region
|
||
replication of that, nothing more. It does **not** generate embeddings, store
|
||
video/blobs, run a CDN or transcoder, do auth/moderation/payments, or provide a
|
||
distributed consensus log. Bring your vectors; tidalDB retrieves and ranks over
|
||
them. See [VISION.md](../../VISION.md) for the scope boundary.
|
||
|
||
## Prerequisites
|
||
|
||
- Rust toolchain ≥ 1.91 if running directly (the Docker build pins
|
||
`rust:1.91-bookworm`).
|
||
- `protobuf-compiler` (`protoc`) and a C++ toolchain (`g++`) on the build host —
|
||
`tidal-net`'s build script compiles the WAL-shipping `.proto`, and USearch's
|
||
HNSW core is C++. The Docker image installs both.
|
||
- Docker 25+ if running via container.
|
||
- Port `9500` available (default cluster HTTP listener). Each follower region
|
||
also binds an **OS-assigned loopback port** for its gRPC replication server
|
||
unless you pin one in the topology (see [§3](#3-topology-yaml)).
|
||
|
||
## 1. Launch the cluster locally
|
||
|
||
Cluster mode is gated. Pass `--experimental-cluster` (or set
|
||
`TIDAL_ALLOW_EXPERIMENTAL_CLUSTER=1`) or the server exits with an error
|
||
explaining why.
|
||
|
||
```bash
|
||
TIDAL_ALLOW_EXPERIMENTAL_CLUSTER=1 \
|
||
cargo run -p tidal-server -- \
|
||
cluster \
|
||
--listen 127.0.0.1:9500 \
|
||
--schema tidal-server/config/default-schema.yaml \
|
||
--topology tidal-server/config/default-cluster.yaml \
|
||
--experimental-cluster
|
||
```
|
||
|
||
The default topology spins up three regions (`us-east`, `eu-west`, `ap-south`)
|
||
with `us-east` as leader. On a clean start you will see a `WARN` line stating
|
||
this is experimental and single-process, an `info` line per follower
|
||
(`follower gRPC transport ready`), and finally `listening on http://127.0.0.1:9500`.
|
||
|
||
**Auth:** set `TIDAL_API_KEY=<secret>` to require `Authorization: Bearer <secret>`
|
||
on the data and `/cluster/*` mutation routes. If it is **unset the server runs
|
||
UNAUTHENTICATED and logs a WARN** — never expose an unauthenticated cluster
|
||
beyond loopback. Health probes and `/openapi.json` are always unauthenticated.
|
||
|
||
Useful environment variables (shared with standalone):
|
||
|
||
| Var | Effect |
|
||
|-----|--------|
|
||
| `TIDAL_ALLOW_EXPERIMENTAL_CLUSTER=1` | Opt in to cluster mode (alternative to `--experimental-cluster`). |
|
||
| `TIDAL_API_KEY` | Bearer token for protected routes. Unset ⇒ unauthenticated + WARN. |
|
||
| `TIDAL_CONFIG` | Config dir holding `default-schema.yaml` / `default-cluster.yaml` (used when `--schema` / `--topology` omitted). |
|
||
| `PORT` | Listen address. A bare port (`9500`) normalises to `0.0.0.0:9500`. |
|
||
| `TIDAL_SERVER_LOG` | `tracing` filter (default `info`). |
|
||
|
||
## 2. Launch via Docker
|
||
|
||
```bash
|
||
# Build the image once (build context is the repo root).
|
||
docker build -f docker/cluster/Dockerfile -t tidaldb:cluster .
|
||
|
||
# Run (press Ctrl+C to stop). The image already sets
|
||
# TIDAL_ALLOW_EXPERIMENTAL_CLUSTER=1 so cluster mode starts; it still logs the
|
||
# loud experimental WARN.
|
||
docker run --rm -p 9500:9500 tidaldb:cluster
|
||
```
|
||
|
||
The image bakes the default schema/topology under `/etc/tidal-server` and uses
|
||
`ENTRYPOINT ["tidal-server"]` + a `cluster …` `CMD`, so you can override the
|
||
subcommand (e.g. `docker run tidaldb:cluster standalone …`) or supply your own
|
||
config files:
|
||
|
||
```bash
|
||
docker run --rm -p 9500:9500 \
|
||
-e TIDAL_API_KEY=changeme \
|
||
-v "$PWD/configs/my-schema.yaml:/srv/schema.yaml:ro" \
|
||
-v "$PWD/configs/my-topology.yaml:/srv/topology.yaml:ro" \
|
||
tidaldb:cluster \
|
||
cluster \
|
||
--listen 0.0.0.0:9500 \
|
||
--schema /srv/schema.yaml \
|
||
--topology /srv/topology.yaml
|
||
```
|
||
|
||
The container ships a `HEALTHCHECK` that curls `/health`. For Kubernetes/Compose
|
||
deployment patterns see [Cross-references](#cross-references).
|
||
|
||
## 3. Topology YAML
|
||
|
||
The topology file declares regions and the leader. Minimal default
|
||
(`tidal-server/config/default-cluster.yaml`):
|
||
|
||
```yaml
|
||
regions:
|
||
- name: us-east
|
||
- name: eu-west
|
||
- name: ap-south
|
||
leader: us-east
|
||
# Optional: OS worker threads serving cluster write/heal requests (gRPC ship).
|
||
# Defaults to available parallelism when omitted.
|
||
# write_workers: 4
|
||
```
|
||
|
||
Fields:
|
||
|
||
| Field | Required | Meaning |
|
||
|-------|----------|---------|
|
||
| `regions[].name` | yes | Region name used everywhere in the HTTP API (`?region=`, `/cluster/promote`, etc.). Must be unique. |
|
||
| `regions[].grpc_addr` | no | This region's gRPC replication bind address, e.g. `"127.0.0.1:9601"`. **Omit it** for the single-process default — the server allocates a free loopback port and self-heals a bind race by retrying on a fresh port. An explicit address is only needed for the future multi-process deployment (m8p10) and is tried exactly once. |
|
||
| `leader` | yes | Must name one of the declared regions. Writes route here. |
|
||
| `write_workers` | no | Size of the runtime-free OS-thread pool that runs the blocking gRPC segment-ship on `/signals` and `/cluster/heal`. Bounds write concurrency on the hottest path. |
|
||
|
||
The schema YAML (signals / text_fields / embedding_slots / profiles) is the same
|
||
format the standalone server loads — see [API.md](../../API.md) and
|
||
[QUICKSTART.md](../../QUICKSTART.md) for the full schema/profile grammar and the
|
||
25 built-in ranking profiles. Topology only adds the region layout above.
|
||
|
||
> **Personalization correctness note.** Preference-vector personalization (the
|
||
> taste vector that powers `for_you`) only updates for signals declared
|
||
> `positive_engagement: true` in the schema. The cluster `/signals` route writes
|
||
> **global** signals (no `user_id` / `creator_id` context), so it never folds
|
||
> item embeddings into a user's taste vector regardless of the
|
||
> `positive_engagement` flag. Personalized writes go through the embedded engine
|
||
> (`signal_with_context`), not this HTTP route.
|
||
|
||
## 4. gRPC replication transport (tidal-net)
|
||
|
||
Replication between regions is the real `tidal-net` `WalShipping` gRPC service,
|
||
not in-process channels. The facts you need to operate and firewall it:
|
||
|
||
| Property | Value | Notes |
|
||
|----------|-------|-------|
|
||
| Service | `WalShipping` (proto `tidal.replication.v1`) | RPCs: **`ShipSegment`** (unary; the production replication path) and **`Heartbeat`** (ControlPlane health). `StreamSegments` is declared but **not implemented** — segment delivery is the unary `ShipSegment`. |
|
||
| Default port | `59529` | In tidalDB's reserved dev band `59520–59529`. In single-process cluster mode each follower binds an **auto-allocated** loopback port instead (or the topology's `grpc_addr`). |
|
||
| Transport security | mTLS via `TlsConfig` (`ca_cert`, `server_cert`, `server_key`, optional `client_cert` / `client_key`) | `insecure = true` (plaintext) is the single-process loopback default. A peer with no TLS config **must** set `insecure`. |
|
||
| Max payload | **64 MiB** | Both encoder and decoder codec limits are raised to this; pinned by a compile-time assert to the engine's `InProcessTransport` limit so both ends agree. WAL segments default to 16 MiB. |
|
||
| Circuit breaker | per-peer, **threshold 5**, **reset 30s** | After 5 consecutive ship failures the breaker opens; it stays open for 30s, then the next attempt probes (HalfOpen → Closed on success, Open again on failure). Backpressure (channel full) does **not** trip it. |
|
||
| Timeouts (defaults) | connect 5s, request 10s, keep-alive PING every 10s / 5s ACK | A blackholed peer fails fast instead of stalling the single-threaded shipper. |
|
||
|
||
`ShipSegment` carries the WAL segment id, BLAKE3-validated payload bytes, event
|
||
count, and the leader's authoritative `leader_last_seq` (so a follower can
|
||
advance its replication-lag high-water-mark even for an all-local segment that
|
||
filters to empty). The follower's segment-receiver thread drains and applies it
|
||
on arrival.
|
||
|
||
> **Operational guard:** the gRPC replication ports and the Prometheus `/metrics`
|
||
> endpoint are **UNAUTHENTICATED**. Bind them to loopback or a cluster-internal
|
||
> network only — never the public interface.
|
||
|
||
## 5. Core HTTP API
|
||
|
||
All routes are JSON unless noted. Examples assume `BASE=http://localhost:9500`
|
||
and, when `TIDAL_API_KEY` is set, `AUTH='-H "Authorization: Bearer $TIDAL_API_KEY"'`.
|
||
Drop the `-H` header when running unauthenticated.
|
||
|
||
Middleware mirrors standalone: 30s request timeout (408), 100 max in-flight
|
||
(429), 2 MB body limit (413), and an `x-request-id` on every response. Health
|
||
probes sit **outside** the load-shedding stack so liveness/readiness are never
|
||
queued or timed out under saturation.
|
||
|
||
### Health
|
||
|
||
```bash
|
||
curl "$BASE/health" # 200 ok / 503 while draining; reports leader + item count
|
||
curl "$BASE/health/startup" # always 200
|
||
curl "$BASE/health/live" # always 200
|
||
curl "$BASE/openapi.json" # served OpenAPI 3.1, UNAUTHENTICATED — canonical HTTP reference
|
||
```
|
||
|
||
The cluster `/openapi.json` is a superset that documents the `/cluster/*` and
|
||
`/sharded/*` routes. It is the machine-readable source of truth for request /
|
||
response shapes.
|
||
|
||
### Register items & embeddings (leader-routed)
|
||
|
||
```bash
|
||
curl -X POST "$BASE/items" \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{ "entity_id": 1, "metadata": { "title": "Jazz Piano", "category": "music" } }'
|
||
# → 201 Created
|
||
|
||
curl -X POST "$BASE/embeddings" \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{ "entity_id": 1, "values": [0.1, 0.2, 0.3, 0.4] }'
|
||
# → 204 No Content
|
||
```
|
||
|
||
Embeddings: tidalDB does **not** generate vectors — the caller brings them. The
|
||
write L2-normalizes and inserts into the HNSW index. Dimensions are **strict**:
|
||
they must equal the slot's declared dimensions (min 2, max 4096) or the insert
|
||
fails, and **zero-norm vectors are rejected**. `RETRIEVE` / `SEARCH` route
|
||
through the **first declared embedding slot only**; multi-modal apps must fuse
|
||
offline or use separate entity kinds.
|
||
|
||
### Record signals (cluster `/signals` = global only)
|
||
|
||
```bash
|
||
curl -X POST "$BASE/signals" \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{ "entity_id": 1, "signal": "view", "weight": 1.0 }'
|
||
# → 204 No Content (see the durability contract in §8)
|
||
```
|
||
|
||
The signal name must be declared in the schema. This route records a **global**
|
||
signal on the leader and ships it to followers; it does not personalize (see the
|
||
personalization note in [§3](#3-topology-yaml)).
|
||
|
||
### Retrieve and search (region-pinned reads)
|
||
|
||
```bash
|
||
# Default read region is the leader.
|
||
curl "$BASE/feed?user_id=42&profile=for_you&limit=20"
|
||
curl "$BASE/search?query=jazz%20piano&user_id=42&limit=5"
|
||
|
||
# Pin a read to a specific region. Followers may lag the leader (and lag jumps
|
||
# during a partition) — use this for canary reads and lag verification.
|
||
curl "$BASE/feed?profile=trending®ion=eu-west"
|
||
```
|
||
|
||
`?region=` accepts any declared region name; an unknown name returns 400. Omit
|
||
it and the read serves from the current leader. `limit` is clamped to 1000 at the
|
||
trust boundary. The `for_you` profile applies the built-in diversity defaults
|
||
(`max_per_creator=2`, `format_mix_max_fraction=0.4`, `exploration=0.1`).
|
||
|
||
## 6. Cluster management API
|
||
|
||
### Check cluster status
|
||
|
||
```bash
|
||
curl "$BASE/cluster/status" | jq
|
||
```
|
||
|
||
```json
|
||
{
|
||
"leader": "us-east",
|
||
"relay_log_len": 125,
|
||
"regions": [
|
||
{ "name": "us-east", "applied_events": 125, "lag_events": 0, "partitioned": false },
|
||
{ "name": "eu-west", "applied_events": 125, "lag_events": 0, "partitioned": false },
|
||
{ "name": "ap-south", "applied_events": 124, "lag_events": 1, "partitioned": false }
|
||
]
|
||
}
|
||
```
|
||
|
||
`lag_events` is `relay_log_len − applied_events` (saturating). A non-zero,
|
||
*growing* lag on a region is the signal that it is partitioned or its
|
||
segment-receiver is wedged.
|
||
|
||
### Promote a new leader
|
||
|
||
```bash
|
||
curl -X POST "$BASE/cluster/promote" \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{ "region": "eu-west" }'
|
||
# → { "ok": true, "leader": "eu-west" }
|
||
```
|
||
|
||
After promotion `/cluster/status` reports the new leader; new writes route there
|
||
and replay to the other regions. An unknown region returns 400.
|
||
|
||
### Simulate a partition & heal
|
||
|
||
```bash
|
||
# Isolate ap-south: leader ships skip this follower, so its lag climbs.
|
||
curl -X POST "$BASE/cluster/partition" \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{ "region": "ap-south" }'
|
||
# → { "ok": true, "partitioned": "ap-south" }
|
||
|
||
# Heal: missed segments are re-shipped over gRPC (blocking, offloaded to the
|
||
# write pool — a saturated pool degrades to 429, not 500).
|
||
curl -X POST "$BASE/cluster/heal" \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{ "region": "ap-south" }'
|
||
# → { "ok": true, "healed": "ap-south" }
|
||
```
|
||
|
||
## 7. Sharded scatter-gather API
|
||
|
||
The `/sharded/*` routes hash-partition entities across regions
|
||
(`hash(entity_id) % num_shards`, using the engine's own `ShardRouter` so writes
|
||
and reads never disagree) and fan a query out to **all** shards, K-way merging
|
||
the per-shard results by score.
|
||
|
||
Write routes (each writes to the single owning shard):
|
||
|
||
```bash
|
||
curl -X POST "$BASE/sharded/items" -d '{ "entity_id": 7, "metadata": { "title": "..." } }' # 201
|
||
curl -X POST "$BASE/sharded/embeddings" -d '{ "entity_id": 7, "values": [0.1,0.2,0.3,0.4] }' # 204
|
||
curl -X POST "$BASE/sharded/signals" -d '{ "entity_id": 7, "signal": "view", "weight": 1.0 }' # 204
|
||
```
|
||
|
||
Read routes (scatter-gather across all shards):
|
||
|
||
```bash
|
||
curl "$BASE/sharded/feed?profile=for_you&limit=20&deadline_ms=50"
|
||
curl "$BASE/sharded/search?query=jazz&limit=10&deadline_ms=100"
|
||
```
|
||
|
||
Each sharded read accepts an optional `deadline_ms` — the **total** scatter
|
||
budget (default 50ms, server-clamped to 10s). The per-shard deadline is
|
||
`deadline_ms − 5ms` network overhead. The response includes a `scatter_gather`
|
||
block:
|
||
|
||
```json
|
||
{
|
||
"items": [ /* merged, deduped, diversity-enforced, re-ranked */ ],
|
||
"total_candidates": 4210,
|
||
"scatter_gather": {
|
||
"degraded": false,
|
||
"shards_queried": 3,
|
||
"elapsed_ms": 12,
|
||
"shard_deadline_ms": 45
|
||
}
|
||
}
|
||
```
|
||
|
||
Degraded semantics: a shard that is partitioned, errors, or misses the deadline
|
||
is reported in `unavailable_shards` (a name list) and flips `degraded: true` —
|
||
it is **never silently dropped**. The merge dedups replicated copies of an entity
|
||
(keeping the best-scoring copy), reconciles `total_candidates` so replicated
|
||
shards are not counted multiple times, and re-enforces `max_per_creator` across
|
||
the merged set.
|
||
|
||
## 8. Write-durability contract (read this before trusting a 204)
|
||
|
||
A `204 No Content` from `/signals` (and the data writes) means **the write is
|
||
durably applied on the leader** — storage updated plus **WAL fsync** — and
|
||
nothing more.
|
||
|
||
- The follower ship is **best-effort**. A ship that fails is logged engine-side
|
||
and queued for re-delivery via the heal / convergence path. The 204 does
|
||
**NOT** assert quorum and does **NOT** assert any follower acknowledged the
|
||
write.
|
||
- If the single process crashes after the 204 but before followers caught up,
|
||
the leader's WAL still has the write (it survives restart from disk); the
|
||
followers will reconcile on the next ship/heal. But because there is **only
|
||
one process**, a crash is total downtime, not a failover — see the status box.
|
||
- A quorum-ack write contract is explicitly future work, tracked with the
|
||
multi-process cluster effort (**m8p10**).
|
||
|
||
In short: **204 = leader durability, not cluster durability.** Design your
|
||
client retries accordingly (the writes are idempotent on `entity_id` + signal).
|
||
|
||
## 9. Failover drill
|
||
|
||
1. **Baseline.** `GET /cluster/status`; confirm the expected leader and
|
||
`lag_events: 0` on every region.
|
||
2. **Pre-seed reads.** Issue a region-pinned read against the target region
|
||
(`?region=eu-west`) to confirm it is serving and roughly caught up.
|
||
3. **Promote.** `POST /cluster/promote { "region": "eu-west" }`.
|
||
4. **Verify.** `GET /cluster/status` now reports `eu-west` as leader. Send a
|
||
write (`POST /signals`) and confirm `relay_log_len` advances and the other
|
||
regions' `applied_events` follow within a heartbeat.
|
||
5. **Cut over traffic.** Point your client's writes at the same `$BASE` (the
|
||
single surface routes writes to whoever is leader) — no client change needed.
|
||
|
||
> Reminder: this is a *leadership move within one process*, not a host failover.
|
||
> It is the right drill for "move the write region during maintenance," not for
|
||
> "survive a machine dying."
|
||
|
||
## 10. Partition drill
|
||
|
||
1. **Baseline.** `GET /cluster/status`; all `lag_events: 0`, all
|
||
`partitioned: false`.
|
||
2. **Inject.** `POST /cluster/partition { "region": "ap-south" }`. Confirm
|
||
`partitioned: true` for that region.
|
||
3. **Write through it.** Send several `POST /signals`. Watch `ap-south`'s
|
||
`lag_events` climb while `applied_events` stalls — the leader's ships to it
|
||
are dropped.
|
||
4. **Read the stale follower.** `GET /feed?region=ap-south` should return the
|
||
pre-partition view (demonstrating eventual, not strong, read consistency).
|
||
5. **Heal.** `POST /cluster/heal { "region": "ap-south" }`. Missed segments
|
||
re-ship over gRPC.
|
||
6. **Verify convergence.** Poll `GET /cluster/status` until `ap-south`'s
|
||
`lag_events` returns to 0 and `partitioned: false`.
|
||
7. **Scatter-gather degradation.** While partitioned, a `GET /sharded/feed`
|
||
should report `degraded: true` with `ap-south` in `unavailable_shards` — not
|
||
an error. Confirm it clears after heal.
|
||
|
||
## 11. Shutdown
|
||
|
||
Send `SIGTERM` (or `SIGINT` / Ctrl+C, or stop the container). The server flips
|
||
readiness to 503, drains in-flight requests, then drops the cluster — which runs
|
||
each region's `TidalDb` shutdown path: checkpoint in-memory signal state → flush
|
||
storage → write the WAL checkpoint marker + **fsync** → join the WAL, sweeper,
|
||
checkpoint, and text-syncer threads. The drop is idempotent. You will see
|
||
`cluster shutdown: closing all nodes (checkpoint + WAL fsync)`.
|
||
|
||
## Cross-references
|
||
|
||
- **Kubernetes deployment** — `docs/runbooks/kubernetes.md` *(planned for the
|
||
m8p10 multi-process work; not yet written — until then deploy a single
|
||
standalone node per the production guidance in the status box, and use the
|
||
Docker `HEALTHCHECK` / `/health` probe as the readiness gate).*
|
||
- **Server deployment guide** — `docs/guides/server-deployment.md` *(planned;
|
||
not yet written — see [API.md](../../API.md) and [QUICKSTART.md](../../QUICKSTART.md)
|
||
for the current server config and route reference).*
|
||
- **Monitoring & alerts** — [docs/ops/monitoring.md](../ops/monitoring.md)
|
||
(Prometheus scrape config, replication-lag and WAL metrics, recommended
|
||
alerts; remember the `/metrics` endpoint is unauthenticated — bind it
|
||
internally).
|
||
- **Roadmap / m8p10 (true multi-process HA)** —
|
||
[docs/planning/ROADMAP.md](../planning/ROADMAP.md) for the distributed-fabric
|
||
status, the m8p1–m8p9 completed phases, and the outstanding multi-process /
|
||
tier-3 chaos work.
|
||
- **API & schema reference** — [API.md](../../API.md),
|
||
[QUICKSTART.md](../../QUICKSTART.md), and the live `/openapi.json` document.
|
||
- **Scope & vision** — [VISION.md](../../VISION.md).
|
||
```
|
||
|