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:
- Global tokens, from
{auth, #{tokens => [Bin]}}. They open every route. Pass a list so you can rotate. Comparison is constant time. - Capability tokens (
bsp_...), minted bybarrel_capsfor one space. They authenticate the agent-layer routes, and the/db/:dbsurface scoped to the space they grant. The server maps method and path to a required right (reads needread, writes and push needwrite) and checks the database is the granted space.
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
- The changes feed returns JSON by default. Request
Accept: text/event-stream(or?feed=sse) for Server-Sent Events. The?since=<cursor>parameter takes a cursor from a prior response'slastfield. - CORS runs in front of auth, so preflights answer 204 without a token and 401
bodies still carry CORS headers.
exposedefaults to thex-barrel-*headers a client needs to fold the HLC clock. - The database manager does not trap exits: if an open store crashes, the manager restarts with an empty cache and databases reopen on the next request.
- gRPC, WebTransport, and a unix-socket adapter are later phases.