barrel_server

The multi-protocol server for the barrel edge database. It exposes the barrel API (documents, attachments, vectors, search, changes, timeline) as a REST/JSON API over HTTP/1.1 and HTTP/2 using livery, and the same data as an MCP endpoint for agents. It holds no database logic: every handler calls barrel through a database lifecycle manager.

You need this when you want to reach a barrel database over the network (other languages, remote clients, agents) instead of embedding it in an Erlang application. For embedded use, depend on barrel directly and skip this app.

To serve barrel's routes from your own livery service (under a sub-path, with your own auth), use barrel_server_api:routes/router rather than the standalone listener; see the embedding guide.

Documentation | HexDocs | Repository

Build and run

barrel_server is opt-in, behind the umbrella server profile (it pulls livery and its transports). It is not part of the default embeddable build.

$ rebar3 as server compile
$ rebar3 as server shell
1> application:ensure_all_started(barrel_server).

Configuration

All keys live in the barrel_server app env. Set them in sys.config, or with application:set_env/3 after the app is loaded (loading resets the env from the .app file, so a set_env before ensure_all_started/1 is discarded).

[{barrel_server, [
{http_port, 8080},
{data_dir, "/var/lib/barrel"},
%% Request body ceiling. Must clear the largest attachment you sync.
{max_body, 1073741824},
%% Options passed to barrel:open/2 when a database opens lazily.
{open_opts, #{}},
%% Auth. Omit the key entirely to leave the server open. Without an
%% `accept' key this is bearer-only (unchanged). With `accept' it opts
%% into bearer | signed (Ed25519) | mtls; see the synchronization guide.
{auth, #{accept => [bearer, signed],
tokens => [<<"secret-one">>, <<"secret-two">>],
signers => #{<<"node1">> => <<"...32 raw bytes...">>},
skew_ms => 300000}},
%% Listeners. Omit for a single cleartext HTTP/1.1 on http_port; set to
%% serve HTTP/2 and HTTP/3 and TLS (https/http3 and `tls => true' on http
%% require the `tls' config below).
{listeners, #{http => #{port => 8443, tls => true},
https => #{port => 8444},
http3 => #{port => 8444}}},
%% Shared TLS for the TLS listeners. `verify => verify_peer' = mTLS gate.
{tls, #{certfile => "server.pem", keyfile => "server.key",
cacertfile => "ca.pem", verify => verify_peer}},
%% CORS. Omit to emit no CORS headers.
{cors, #{origins => '*'}},
%% MCP endpoint. Enabled by default when the key is absent.
{mcp, #{enabled => true, allowed_origins => any}}
]}].

Endpoints

Databases open lazily on first use and are cached by name.

GET / liveness text
GET /health {"status":"ok"}
PUT /db/:db open/create a database
GET /db/:db database info
DELETE /db/:db close a database
PUT /db/:db/doc/:id body = JSON document
GET /db/:db/doc/:id fetch a document
DELETE /db/:db/doc/:id delete a document
GET /db/:db/doc/:id/_versions live versions (conflict siblings)
GET /db/:db/doc/:id/_versions/:rev one version's body
POST /db/:db/_bulk_docs {"docs":[...]} -> {"results":[...]}
POST /db/:db/_bulk_get {"ids":[...]} -> {"results":[...]}
POST /db/:db/find body = query, returns rows
POST /db/:db/query BQL: {"query":"...","max_rows":N,"deadline_ms":N}
GET /db/:db/query BQL via ?q= (and ?max_rows=, ?deadline_ms=)
GET /db/:db/changes changes feed (JSON, or SSE)
GET /db/:db/_history provenance history
GET /db/:db/_timeline timeline info
POST /db/:db/_timeline/branch fork a timeline
POST /db/:db/_timeline/merge merge a timeline
PUT /db/:db/doc/:id/att/:name body = raw bytes
GET /db/:db/doc/:id/att/:name fetch attachment bytes
DELETE /db/:db/doc/:id/att/:name delete attachment
POST /db/:db/vector {"id","text","metadata","vector"}
POST /db/:db/search/vector {"vector":[...],"k":10}
POST /db/:db/search/bm25 {"query":"...","k":10}
POST /db/:db/search/hybrid {"query":"...","k":10}

Replication

Replication runs over the wire against these endpoints. A remote barrel pulls and pushes through them; you do not call them by hand.

GET /db/:db/_sync/info peer id, HLC, sync state
POST /db/:db/_sync/hlc fold the peer's clock
POST /db/:db/_sync/changes changes since a version vector
POST /db/:db/_sync/diff which versions the peer is missing
GET /db/:db/_sync/doc/:id fetch one version
PUT /db/:db/_sync/doc/:id push one version
GET /db/:db/_sync/local/:id replication checkpoints
PUT /db/:db/_sync/local/:id
DELETE /db/:db/_sync/local/:id
GET /db/:db/_sync/att_changes attachment feed
POST /db/:db/_sync/att_diff
GET /db/:db/_sync/att/:id/:name
PUT /db/:db/_sync/att/:id/:name
DELETE /db/:db/_sync/att/:id/:name

Agent layer

Spaces, capability grants, sessions, and handoffs from barrel_spaces.

POST /spaces create a space
GET /spaces list spaces
GET /spaces/:space space info
DELETE /spaces/:space drop a space
POST /spaces/:space/grants mint a capability token
GET /spaces/:space/grants list grants
DELETE /spaces/:space/grants/:token_id revoke a grant
POST /spaces/:space/sessions open a session
GET /spaces/:space/sessions list sessions
GET /spaces/:space/sessions/:sid session info
DELETE /spaces/:space/sessions/:sid close a session
POST /spaces/:space/sessions/:sid/touch extend the TTL
POST /spaces/:space/sessions/:sid/messages append a message
GET /spaces/:space/sessions/:sid/messages read messages
PUT /spaces/:space/sessions/:sid/data/:key set session data
GET /spaces/:space/sessions/:sid/data/:key read session data
POST /handoffs offer a handoff
GET /handoffs list handoffs
POST /handoffs/accept accept a handoff
POST /handoffs/complete complete a handoff

Contexts and working sets

One BQL statement over several databases, local, imported or on other barrel_server nodes, and the working sets that keep local copies for offline use. See the contexts guide.

POST /contexts register a context card
GET /contexts list cards (?q= discover, ?prefix=, ?unlisted=true)
GET /contexts/:id read a card (id or URL-encoded name)
DELETE /contexts/:id unregister a card
POST /contexts/_query {"query":"...","contexts":[...]} or {"working_set":"..."}
GET /contexts/_capabilities query shapes, merges, limits
GET /contexts/_offline offline mode
PUT /contexts/_offline {"offline":true|false}
POST /worksets create a working set
GET /worksets list working sets
GET /worksets/:ws one working set
DELETE /worksets/:ws delete it and its slices
POST /worksets/:ws/members attach a context
DELETE /worksets/:ws/members/:ctx detach a context
POST /worksets/:ws/_materialize save a query's documents as slices
POST /worksets/:ws/_import import an exported snapshot

Errors from these routes share one body: {"error": code, "message": ..., "hint": ..., "details": {...}}, with an HTTP status per code. Capability tokens may read cards and run context queries (each local member is checked as a POST /db/:db/query on that database); registering cards, working sets, imports and offline mode need a global token.

MCP

When mcp is enabled, /mcp serves the Model Context Protocol (POST, GET, DELETE, OPTIONS) over the same databases: resources, tools, and the agent layer. It carries its own origin policy, so the CORS middleware skips it.

The context tools mirror the routes above: context_capabilities, context_list, context_discover, context_inspect, context_query, context_attach, context_detach, context_materialize, context_import, context_working_sets, context_working_set_delete, context_offline. A query no context answered is an MCP error result.

Examples

$ curl -X PUT localhost:8080/db/mydb
{"ok":true,"db":"mydb"}
$ curl -X PUT localhost:8080/db/mydb/doc/a \
-H 'content-type: application/json' -d '{"title":"hello"}'
{"id":"a","ok":true,"rev":"0000019f46b4c08900000000@8c0010c983917d4b"}
$ curl localhost:8080/db/mydb/doc/a
{"_rev":"0000019f46b4c08900000000@8c0010c983917d4b","id":"a","title":"hello"}
$ curl localhost:8080/db/mydb/changes
{"last":"AAABn0a0wIkAAAAA","changes":[{"id":"a","rev":"0000019f46b4c089...",
"hlc":"AAABn0a0wIkAAAAA","changes":[{"rev":"0000019f46b4c089..."}],
"num_conflicts":0}]}
$ curl localhost:8080/db/mydb
{"name":"mydb","config":{},"db_path":"/var/lib/barrel/mydb","att_floor":null,
"history_floor":null,"keyspace":"mydb","retention_period":2592000}

A BQL query streams NDJSON: one {"row":...} line per row and a final {"meta":...} line. The meta carries bound (limit_reached or exhausted) and the observed version (instance_id, last_seq); a vector_top_k answer adds embedding: {fingerprint, distance, dimensions}. max_rows is capped at 1000 and deadline_ms at 300000; past the deadline the stream ends with {"error":"deadline"} and no meta.

$ curl -XPOST localhost:8080/db/mydb/query -H 'content-type: application/json' \
-d '{"query":"SELECT id FROM c LIMIT 1","max_rows":10}'
{"row":{"id":"a"}}
{"meta":{"bound":"limit_reached","has_more":false,"instance_id":"03798dac60334e1e","last_seq":"AAABoN0VVc4AAAAA"}}

A rev is a version token, <hex(hlc)>@<author>: the HLC of the write and the id of the database that authored it. There is no revision tree.

Authentication

Omit the auth key and the server stays open. Configure it and every route except /health requires Authorization: Bearer <token>.

Two kinds of bearer are accepted:

Unmapped routes answer 403, so a new route has to be classified before it can be reached with a capability token. Bad or revoked tokens answer 401.

Upgrading

barrel_server 1.10.0 requires barrel ~> 1.10 and barrel_ngram ~> 0.11.1. barrel_ngram 0.11 writes manifest version 3; ngram_search opens corpora with on_legacy => reindex, so an older corpus is rebuilt once on first search. Upgrade barrel_server and barrel_ngram together: barrel_server 1.7.2 pins barrel_ngram ~> 0.10, and forced onto 0.11 it does not rebuild older corpora, so its search answers corpus_not_open.

Notes