Running the server
Expose a barrel database over HTTP/1.1 and HTTP/2 with barrel_server, including endpoints, auth, and CORS.
barrel_server exposes the barrel database over HTTP/1.1 and HTTP/2 (REST/JSON)
using livery. It holds no database logic: every handler calls the barrel
module through a database lifecycle manager. Read this when you want to reach a
barrel database over the network instead of embedding it.
When to use it
- You want HTTP access to documents, attachments, vectors, search, and the changes feed (from other languages or remote clients).
- For in-process Erlang use, embed
barreldirectly instead (see the embedding guide).
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 shell
1> application:ensure_all_started(barrel_server).
Configure with the barrel_server app env: http_port (default 8080) and
data_dir (where databases are stored). Set them before the app starts, for
example in sys.config.
Endpoints
Databases open lazily on first use through Barrel’s database lifecycle manager
(barrel_dbs): handles are cached by name, idle databases close after
dbs_idle_timeout (barrel app env, default 5 minutes, 0 disables), and
dbs_max_open evicts the least recently used past a cap.
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 (?purge=true deletes)
PUT /db/:db/doc/:id body = JSON document
GET /db/:db/doc/:id fetch a document
DELETE /db/:db/doc/:id delete a document
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 (ndjson rows; SUBSCRIBE over SSE)
GET /db/:db/changes changes feed (JSON, or SSE via Accept)
GET /db/:db/_history audit trail (see audit-provenance guide)
GET /db/:db/doc/:id/_versions[/:rev] past versions and bodies
GET /db/:db/_timeline lineage; POST .../branch, .../merge
POST /db/:db/_sync/* replication wire (see synchronization)
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}
POST|GET /spaces, /spaces/:space, .../grants, .../sessions, /handoffs
the agent layer (see the spaces guide)
POST|GET /mcp the MCP endpoint (see the mcp guide)
Auth
Unconfigured, the server is open. Set bearer tokens to lock it:
{barrel_server, [{auth, #{tokens => [<<"s3cret">>]}}]}
Every route except /health then requires Authorization: Bearer <token>.
Two kinds of bearer: global tokens (the list above, full access, a list
makes rotation possible) and capability tokens (bsp_..., issued per space
by barrel_caps). A capability token authenticates the /spaces and
/handoffs routes, and its own space’s /db/:db/* routes when :db is the
granted space: read opens the pull leg (GETs, changes, query,
search, and the _sync reads), write adds document writes and the push
leg (_sync/doc PUT, _sync/local and _sync/att writes). Database
lifecycle (PUT/DELETE /db/:db), _timeline, and any unmapped route stay
off-limits to capability tokens (403, fail closed); dead or wrong-space
tokens answer 401. /mcp authenticates through its own provider covering
both kinds. See spaces, mcp, and
barrel-lite.
CORS
Browser clients need CORS. Unconfigured, no CORS headers are sent; set an origin policy to enable it:
{barrel_server, [{cors, #{
origins => '*', %% or [<<"https://app.example">>]
expose => [<<"x-barrel-hlc">>, %% default; the client folds this
<<"x-barrel-digest">>, <<"x-barrel-att-length">>],
max_age => 600
}}]}
Preflight OPTIONS requests are answered without a bearer, and error
responses still carry CORS headers so browser JS can read them. /mcp keeps
its own origin policy. See barrel-lite.
Examples
$ curl -X PUT localhost:8080/db/mydb
{"db":"mydb","ok":true}
$ curl -X PUT localhost:8080/db/mydb/doc/a \
-H 'content-type: application/json' -d '{"title":"hello"}'
{"id":"a","ok":true,...}
$ curl localhost:8080/db/mydb/doc/a
{"_rev":"1-...","id":"a","title":"hello"}
$ curl -X POST localhost:8080/db/mydb/_bulk_docs \
-H 'content-type: application/json' -d '{"docs":[{"id":"b"},{"id":"c"}]}'
{"results":[{"id":"b",...},{"id":"c",...}]}
$ curl localhost:8080/db/mydb/changes
{"changes":[{"id":"a","rev":"1-...","hlc":"..."}],"last":"..."}
Notes
- The changes feed returns JSON by default. Request
Accept: text/event-stream(or?feed=sse) for Server-Sent Events (one-shot: the current window then close).?feed=continuousholds the SSE stream open, pushing each change as a data line with a 30s heartbeat, until the client disconnects.?since=<cursor>takes a cursor from a prior response’slastfield (or a change’shlc). - Databases open with the default vector store (768-dim, BM25 off). The
/search/bm25and/search/hybridendpoints need BM25 enabled, and hybrid needs an embedder. - Optimistic concurrency:
PUT /db/:db/doc/:idwith a_revin the body that is not the current winner answers 409{"error":"conflict"}. - Replication over the wire ships today (the
/db/:db/_sync/*endpoints; see synchronization). gRPC, HTTP/3, WebTransport, a unix-socket adapter, and OpenAPI are later phases.