Server Mode
To execute a deployed project remotely, start barca serve there and use an explicit URL:
barca get daily_report --server http://barca.internal:8274barca run deploy --server http://barca.internal:8274barca history --server http://barca.internal:8274This runs the server’s code and keeps history there; it does not upload local edits. No shared artifact store is needed for this path. Keep the server on a private network or behind an authenticating proxy (https://barca.sh/deploying/).
Local computation with shared results
Section titled “Local computation with shared results”Sharing results between machines means one barca serve that everyone points at. The server
is the ledger: it holds the one record of which results exist (run hashes, artifact paths, run
history) and it is the only process that writes it. The artifact bytes stay in the store
([remote], barca docs remote); the server never moves them. Server mode is experimental
(barca docs contract). To set it up in three steps, see
https://barca.sh/team-server/.
Server mode remains experimental after 0.36.0. Runs of one server compute a matching asset step
once, experimentally since 0.39.4 (barca docs serve-ops, “Runs share the steps they have in
common”): cancelling the run that computes it makes the waiting run execute it again, the result
reaches the waiting run before the computing run’s row is durable, and a run that decided the step
before another run finished it can execute it a second time. A stable designation
also requires closing those gaps (#654), bounded cold/full-domain planning and
inspection (#655, #656), and sustained multi-host proxy/store validation at the supported
scale. Opt-in tokens, bounded event replay and configurable backups address deployment
capabilities; they do not establish those remaining properties. Tokens grant full project
access, without accounts, roles or tenant isolation.
With a server configured, a command does one of three things:
| You type | get / run |
status / history / stats |
sql |
|---|---|---|---|
| nothing extra | run on this machine, against the server’s ledger | answered by the server | views from the server’s ledger, query run here |
--server [<url>] |
run on the server, printed here | answered by the server | views from that server, query run here |
--local |
run on this machine against its own ledger; nothing is read from or sent to the server | answered from this machine’s records | this machine’s results |
barca get daily_report # here; cache hits come from the server's ledgerbarca get daily_report --dry-run # what would run, decided against the ledgerbarca run deploy # a task, run here, recorded on the serverbarca get daily_report --server # run it on the configured serverbarca get daily_report --server https://barca.internal.example.combarca run deploy --server --agent # a task on the server, with the agent linesbarca status # the server's ledger, and what is herebarca history -l 20 # the server's runsbarca stats daily_report # timings from the serverbarca sql "select * from daily_report" # the team's result, queried herebarca sql "select * from barca.runs" --local # this machine's own runsbarca status --problems # what needs attention on the serverbarca doctor # this machine and the server: version, store, load errors, schedulesbarca list --server --json # the deployed nodesbarca get daily_report --local # this machine only; the server is not touchedWhich server
Section titled “Which server”Configure it once in barca.toml, with the store the server uses:
# barca.toml on a laptop[server]url = "https://barca.internal.example.com"
[remote]uri = "s3://team-bucket/barca" # the store the server usesor per shell with export BARCA_SERVER=https://barca.internal.example.com. BARCA_SERVER
beats [server] url; an empty BARCA_SERVER= counts as unset. --server <url> names a server
for one command. The URL is http:// or https://, with any path prefix a proxy adds
(https://host.example.com/barca).
The server is an ordinary barca serve of the project with the same [remote] and no
[server] section (barca docs scheduling, “Calling the server”).
- A command that uses the configured server prints
[barca] server <url> (<where it came from>)once on stderr. A URL typed after--serveris not announced. --serverwith no URL uses the configured server. With nothing configured it is a usage error (exit 2).--serverfollowed by a target or a flag is bare--server:barca get --server totalgetstotalfrom the configured server. A word with://after it is its URL, which must start withhttp://orhttps://.--localignores the configured server for one command;--localwith--serveris a usage error.--serverand--localapply toget,run,status,list,history,stats,logs,sqlanddoctor.serve,plan,docsandversionalways run on this machine;--serveror--localon them is a usage error.barca listwithout--serverreads this machine’s files.barca sqlalways runs its query on this machine: a server only says which results its views read (barca docs sql, “With a server”); the server never runs SQL.doctorruns its checks here and, with a server (configured, or--server), checks the server too;--localleaves it out (barca docs doctor, “Checking a server”).
A local run against the server’s ledger
Section titled “A local run against the server’s ledger”Without --server or --local, barca get and barca run run on this machine with this
machine’s code. In order:
- The command prints
[barca] server <url> (<source>)and asksGET /health. The server must speak this barca’s API version and write to the same store as this project’s[remote]. - Runs this machine could not post before are posted again (below).
- One
POST /ledger/lookupasks about every run hash of the plan that is known before anything runs. A step downstream of a sensor (its output is part of the hash) is asked about when its phase is decided, one request per such phase. - A hit is a cache hit as usual: its artifact is fetched from the store when something reads it. A local copy whose SHA-256 differs from the digest the server recorded is replaced by the store’s copy. A miss runs here.
- After the run’s uploads are confirmed,
POST /ledger/runssends the run and its step rows (store paths and digests). The server checks that every object exists in its store with that digest (an object store, which has no content checksum to compare, by size) before it records anything, and records the run withtriggered_by: clientand this machine’s run id asclient_run_id(barca history --jsonshows both).
--dry-run is decided against the ledger and posts nothing. Only assets are posted: tasks and
sensors run here and stay in this machine’s history.
The lookup is by run hash only. Two people with the same code share results; a person who edited
report.py gets misses for the nodes the edit changed and hits for the rest, and the rows they
post are keyed by their own hashes.
When the post does not go through
Section titled “When the post does not go through”The run is a success here whatever happens to its post: the exit code and the output do not
change. If the server refuses the post (409: an object is missing, outside its store, or does
not have the posted digest) or cannot be reached, the command prints
[barca] warning: run <id> was not recorded on the server <url>: <why>; it stays on this machine (unsynced) and the next command against the server sends it againand keeps the post in this machine’s database (the ledger_posts table, state unsynced).
barca status shows such a result as unsynced with cache.reason: "not_in_ledger" (below).
The next get or run against the server posts it before its own lookup and prints
[barca] recorded run <id> on the server (run <server id>), or
[barca] warning: run <id> is still not recorded ... if it is refused again. A result whose
object went missing is a miss on the ledger, so the next get that needs it computes and posts
it again, after which the refused post is accepted too.
Only successful runs are posted. A run that failed keeps the steps that succeeded on this
machine; they are computed again by the next machine that needs them. A run some of whose
results could not be uploaded to the store (their upload is pending, barca docs remote) is
held: it prints [barca] warning: run <id> is not recorded on the server <url> yet: some of its results are not in the artifact store yet ..., and its post is kept unsynced (barca status
shows its results not_in_ledger) without being sent, since the server would refuse it. The
next command that opens the store uploads the pending results first and then posts the run
([barca] recorded run <id> on the server ...), before it decides its own steps, which are
then hits; other machines get hits from then on. A pending result whose local file is gone is
dropped (barca docs remote) and taken out of the post, and the rest of the run is posted.
A server that is draining before it stops (SIGTERM, barca docs scheduling) refuses posts with
503; the run stays unsynced here and the next command against the server sends it again.
Backups
Section titled “Backups”The server backs up its database to the store; the machines that use it take no backups. What
a restore on the server takes, and the client backups older barca versions left in the store,
are in barca docs backup, “With a server”.
Two machines, same run hash, different bytes
Section titled “Two machines, same run hash, different bytes”When the ledger already holds a run hash with another digest (a function that is not deterministic, computed twice), it keeps its row and tells the poster, which prints
[barca] warning: <node> produced different bytes for the same code and inputs; the function is not deterministiconce, and marks the step bytes_changed: true in its --json output. Artifacts are addressed
by run hash, so the second machine’s upload has replaced the object in the store; a later fetch
of that result notes the mismatch and uses the store’s copy.
status, history, stats, logs and sql with a server
Section titled “status, history, stats, logs and sql with a server”With a server configured, barca status, barca history, barca stats and barca logs answer
from the server, as --server does, after printing [barca] server <url> (<source>). The server
answers for its deployed code. barca logs reads the server’s runs (GET /runs/{run_id}/logs,
and -f its event stream); the lines of a run this machine ran against the ledger stay here,
with --local (barca docs logs, “Debugging a live pipeline”). Nothing falls back to this
machine, but barca logs RUN_ID for a run only this machine has fails (exit 2) with the
remediation this run is on this machine: barca logs RUN_ID --local, and barca logs without a
run id adds [barca] note: this machine has a newer run <id> (...); barca logs --local reads it
on stderr when this machine’s newest run is newer than the server’s.
barca history’s filters (--status, --target, --triggered-by, --since) are applied by
the server (GET /history?status=...&target=...&triggered_by=...&since=...; a relative
--since counts back from the server’s clock), and barca history RUN_ID reads
GET /runs/{run_id}: the run, its steps with their attempts, run hashes, artifacts, errors and
tracebacks (at most 1000 steps unless --limit N or --all asks for more; steps_page says how
many there are). For a finished run whose plan the server can still rebuild from the sources,
steps_page.nodes lists every planned node in plan order with a status: ran, cached, or
skipped with blocked_by naming the failed node upstream of it (experimental; the local
barca history RUN_ID lists only the nodes with recorded steps). A run only this machine has (a failed run against the ledger is never posted) fails
(exit 2) with the remediation this run is on this machine: barca history RUN_ID --local.
barca status (without --server) also says what this machine holds. Each node in --json
gets local: true when this machine has a copy of the result the server would serve (its
mirror of the store object exists under this machine’s artifact directory), else false.
barca sql queries the server’s result either way, read from the store. A node the server does
not have a result for, whose result
this machine computed and could not post yet, is unsynced with cache.reason: "not_in_ledger" and local: true; the table prints unsynced not in ledger, and
summary.unsynced counts it. A result is matched by node and run hash, so one computed here
with code the server does not run keeps the server’s state. A partitioned node has no single
artifact, so its local is false.
barca sql asks the server which result each view reads (GET /sql/views) and reads barca’s
tables (barca.runs, barca.materializations, barca.nodes) from its records, then runs the
query here over the store; the server never runs SQL (barca docs sql, “With a server”). An
unsynced result is not mixed into a view: the error or note says so and names --local.
barca status --problems (or --state <states>) keeps the nodes that need attention, the
server’s failures, stale results and failing or paused schedules among them, plus this
machine’s not_in_ledger results; each scheduled node carries the server scheduler’s view in
schedule (barca docs status, “Schedules”). barca doctor checks the server itself.
barca history --reconcile writes this machine’s history and runs here. Add --local to see
this machine’s own runs, statuses and timings.
Running a command on the server: --server
Section titled “Running a command on the server: --server”The server runs its deployed files. Files on the command line are optional: without them the server uses every deployed file; with them it reads only those, as a local command does, and a directory selects the deployed files under it. A file the deployment does not have is a usage error listing the deployed files. When a file you named differs from the deployed one, barca prints one note and continues:
[barca] note: pipeline.py differs from the deployment; the server runs the deployed versionYour edits run only on this machine (leave out --server), or after you deploy them.
--env must name the server’s environment; another is a usage error naming the server’s.
What prints
Section titled “What prints”For every command the client prints what a local run against the same project and history
prints: the stdout JSON key for key (built by the same functions), the stderr lines in the same
order, the exit codes 0, 1, 2, 3 and 130. --json, --pretty, -o path, -o value and -o json|pretty on run, --fields, --limit and --agent apply here as they do locally. A
get prints the server’s artifact paths (paths by default, as barca get --help says): its
--json final_output is the server’s _barca_artifact pointer. With -o value the server
reads the values (RunRequest.values) and they print as locally, partitioned targets included.
--partition selects keys across the cone as locally, on get and run (RunRequest.partition).
get -o url is a usage error
(exit 2) with --server: run the command with --local. A step’s own output ([node] line) is
printed on stderr as a local run prints it.
The declared differences:
root, artifact paths,artifact_dirand traceback file names are the server’s.triggered_byinbarca history --jsonisapifor a run started with--server(a command typed here recordsmanual).- The “no .py files” usage error does not occur: the server has its files.
- A local run in a terminal shows a progress bar. The client prints the lines a bar leaves
behind (warnings, notes, the end-of-run line) and, with
--agent, the agent lines. - Lines the Python workers write to stderr themselves (
[barca] importing <module>, and anything a step writes to stderr directly) stay on the server’s stderr. A step’s printed output (stdout) is streamed and printed here as[node] line. - The
[barca] env <env> (...); artifact store <store>startup line describes this machine’s configuration and is not printed for a--servercommand. - In human mode an error’s text is rebuilt from the envelope’s fields; in JSON mode the envelope is the same.
barca status --serverhas nolocalkey: it reports the server alone.- Status
disk_usagedescribes the storage and cache budget of the machine serving the request, across all of its project environments. A local project copy can have different budgeted, backup and physical byte counts, limits or unmeasured values while its graph and cached results are equivalent. The accounting relationship and JSON fields stay the same.
The parity suite (python/tests/test_server_mode_parity.py) runs the contract’s case table
once locally and once through barca serve on a copy of the same project and compares the
normalized output with exactly these differences.
run -o value on a parquet or pickle result prints the pointer, as locally. barca.get(..., server=True) reads the value from GET /runs/{run_id}/output (an unpartitioned target only).
With partition= it asks for the values instead (-o value): the selected keys’ values read on
the server, as JSON (a table as a list of row objects).
A draining server (one that is restarting) accepts a new --server run into its run queue;
the client keeps following it: when the stream breaks it waits up to 60 seconds
(BARCA_SERVER_RECONNECT) for the server to come back and follows the same run id there, and
only then exits 3 (barca docs serve-ops, “Restarting without losing work”). On servers
advertising event resume in /health, reconnects send the last successfully processed
Last-Event-ID; older servers retain replay-count fallback. An attempt change resets live
presentation while following the same run ID, without submitting another run.
Replay is bounded to 65,536 events or 16 MiB per attempt and 64 MiB across the server. If
required events have expired, execution commands warn that their live transcript is incomplete
and poll the recorded outcome every two seconds. A healthy pending/running response can be
followed longer than BARCA_SERVER_RECONNECT; that setting limits transport outages, not run
duration. Ctrl-C still cancels the existing server run during polling. barca logs -f --server
requires a complete transcript and instead exits 3 on an unfillable replay gap, naming the
recorded-log command. The browser also marks incomplete replay and follows the outcome.
Nothing runs here either way.
Ctrl-C (or SIGTERM) asks the server to cancel the run (DELETE /runs/<run_id>) and keeps
printing until the run has stopped; it then exits 130. A second Ctrl-C exits 130 at once, and
the server still stops the run.
Errors
Section titled “Errors”| Situation | Exit | Message names |
|---|---|---|
| the server cannot be reached, answers with something that is not barca, or speaks another API version | 3 | the server URL (and the request); remediation --local for a configured server |
the server writes to another store (or another environment) than this project’s [remote] (a local run) |
2 | both stores |
a server is configured and the project has no [remote] (a local run) |
2 | the missing [remote] section |
| the post after a local run is refused or does not arrive | 0 (the run’s own code) | the run id and why, on stderr |
an error the server answers with (an unknown target, a task given to get) |
as locally | as the local command prints it |
a stream transport outage exceeds BARCA_SERVER_RECONNECT |
3 | the run id |
logs -f --server cannot recover a complete replay |
3 | the run id and recorded-log command |
There is no silent fallback to this machine’s ledger: two people who stop agreeing without
noticing is what the server exists to prevent. To run without the server, pass --local. A
--server run whose stream dropped continues on the server: barca history --server shows how
it ended, and GET <url>/runs/<run_id> has its full outcome.
Python
Section titled “Python”import barca
barca.get("daily_report", server=True) # the configured serverbarca.get("daily_report", server="https://barca.example.com")barca.run("deploy", server=True)barca.api runs the binary, so it follows the same configuration: without server= a
configured server is the ledger of the local run. barca.Client is deprecated in favour of it:
constructing one warns. There is no scheduled removal; a removal lands as a breaking change
in a minor release.
Deploying and the HTTP routes
Section titled “Deploying and the HTTP routes”Run barca serve --host 0.0.0.0 behind a private network or an authenticating, TLS-terminating
proxy; authentication defaults off (https://barca.sh/deploying/). Point clients at the
proxy’s URL, prefix included; https:// works for every request, ledger ones included.
Server URL userinfo is omitted from printed addresses in errors, announcements, ledger
warnings, stream-drop messages, SQL provenance and doctor output, including JSON. For example,
https://user:password@host/barca prints as https://host/barca. Requests retain the configured
credentials; the display change does not change authentication or pending-post matching.
For a bearer-authenticated proxy, set BARCA_SERVER_TOKEN in the client environment.
The CLI and deprecated Python Client send it as Authorization: Bearer on every server
request, including ledger requests, event streams and cancellation. URL userinfo instead
sends Basic credentials. Combining userinfo with BARCA_SERVER_TOKEN is a usage error
(exit 2); a present token must be nonempty ASCII without whitespace or control characters.
These rules apply to URLs from --server, BARCA_SERVER and [server] url alike.
Redirects must stay within the configured origin and proxy path. Known credentials reflected
in HTTP errors are redacted. The reserved BARCA_SERVER_TOKEN variable is removed from
Python worker and transfer-helper environments.
Opt-in project tokens
Section titled “Opt-in project tokens”Set [server] auth = "token" in barca.toml, BARCA_SERVER_AUTH=token, or
barca serve --auth token (flag beats environment, environment beats TOML). The default
is off; only off and token are valid. Use TLS at the proxy for remote connections.
In token mode every API request, including health probes and event streams, needs a bearer
credential. The exact static UI files and GET /auth/status remain public.
On the deployed host, inside the deployed project, run:
barca tokens bootstrapThis local-only command prints one secret code to stdout. It replaces the previous
unconsumed code and expires after 15 minutes. POST /auth/bootstrap with JSON
{"code":"<bootstrap code>"} consumes it once and returns a connection token expiring
in 12 hours. Use that token as a bearer credential to create a named API token with
POST /auth/tokens, JSON {"name":"laptop"}. An optional future UTC expires_at sets
an expiry; omitted or null means no expiry. Creation returns the secret once.
The UI Connect screen accepts either a bootstrap code or an existing API token. Open
API tokens in the sidebar to create named tokens, copy each secret once, list metadata,
and revoke credentials. Tokens have no expiry unless you choose one; the creation form
states this explicitly. A bootstrap connection expires after 12 hours.
The browser keeps its connection in memory and tab-scoped session storage, keyed by server origin and project prefix. Reloading retains the connection; Disconnect clears it and the UI data cache without revoking the token. Authentication failure returns to Connect. Token-mode requests, including fetch-based event streams, omit cookies and reject redirects. No credential goes into a URL or localStorage. A script running in the same origin can read tab storage: host untrusted projects on separate origins, and use TLS for remote access.
Every token grants the server’s full capabilities, including token management. There are
no accounts or roles. Read-only mode still prevents pipeline writes but allows token
administration. GET /auth/tokens lists metadata; DELETE /auth/tokens/{id} revokes
idempotently. Losing every token is recoverable with a new local bootstrap. A reserved
bootstrap connection slot remains available at the 1000 ordinary token limit.
Auth state lives in owner-only .barca/auth/, shared across project environments and
separate from metadata backups. Preserve it separately when migrating; restoring metadata
cannot resurrect a revoked token. Unreadable/corrupt auth state fails closed. Secrets never
belong in URLs, logs, committed configuration or shell arguments. URL Basic credentials
and barca bearer mode cannot share one Authorization header; a layered proxy must forward
bearer credentials and use another mechanism for its own outer authentication.
Missing, invalid, revoked or expired tokens return 401 with WWW-Authenticate: Bearer and
the infra/exit-3 envelope. Token responses use Cache-Control: no-store. Revocation closes
local event streams; external changes and expiry are checked within one second even when
streams are idle. Auth-off mode rejects token administration.
| Route | Body | Answer |
|---|---|---|
POST /ledger/lookup |
{"hashes": [{"node_id", "run_hash"}]} |
{"results": [{"node_id", "run_hash", "exists", "path", "format", "size_bytes", "digest"}]}, one per pair, in order; only exists: false for a miss |
GET /sql/views?files=a.py |
- | {"views": [{"name", "node", "kind", "partitioned", "format", "files": [{"path", "partition", "content_hash"}], "stale", "run_hash"}], "missing": [{"name", "node", "command", ...}], "duplicates": [{"name", "ids"}]}: which result each barca sql view reads |
GET /materializations?limit=N&offset=M |
- | {"materializations": [...], "total", "limit", "offset"}: step results, newest first (barca.materializations); limit defaults to 1000, at most 10000 |
POST /ledger/runs |
{"run": {...}, "steps": [...]}: the run row and its step rows |
201 {"run_id", "bytes_changed"}, or 409 with the error envelope naming the node and nothing recorded; 400 on a server with no store, 403 on a --read-only one, 503 while it shuts down |
A repeated post of a run the server already recorded (same run.run_id) answers 201 with the
run it recorded.
The inspection routes (GET /state, /nodes, /history, /stats/{name} and the others the
CLI and the web UI read) keep what they computed until the sources or the metadata DB change,
and answer a request whose If-None-Match names their last ETag with 304 and no body. So
barca status --server against an unchanged project answers in milliseconds, whatever the
number of partition keys; a finished run or an edit is in the next answer. When deciding the
cache states takes longer than a second (a graph with a million partition keys), a request
after a change waits that second and is answered with the last result meanwhile; what is
running is always current. The routes read the metadata DB in place, beside the runs that
write it, including under --read-only. A current WAL database is opened through read-only
connections: its rows are never written, migrated or checkpointed, although SQLite can create
-wal and -shm beside it. A database needing migration (or not in WAL mode) is copied
on the blocking pool and migrated privately. A missing database stays missing.
Read-only servers warm status once at startup even while another process writes the DB,
then again after settled changes while clients poll.
GET /state?targets=first,second (experimental) computes only those targets and their upstream
dependencies, including durations; use full node ids when names are ambiguous. A target may be
a group id (group:FILE:NAME), meaning every node in the group, and files=a.py adds every
node of a source file: the UI scopes a group or pipeline that way, one short URL at any size.
An absent or empty targets computes every node. Unknown targets, groups or files answer
404; ambiguous names answer 409.
The UI renders the graph, groups and sidebar counts from /assets and /groups first,
shows cache states as checking while /state runs, then fills them in without moving the
graph. A selected pipeline or group requests only its upstream cone; All pipelines requests
the whole graph, and so does the sidebar, once a minute, for its per-pipeline health dots (a
dot appears only when all of its file’s state is known). A status error leaves source metadata visible with the error above it.
The sidebar is the UI’s one navigation: Search (Ctrl/Cmd-K), then Assets, Runs, Schedules,
Tasks and Sensors with how many each lists, then the pipelines, then the docs, the connection,
the time zone and the theme; collapsed, it keeps their icons. A pipeline’s count and dot are of
the open page’s kind (assets, except on Tasks and Sensors), with the same state its header in
the table shows.
On a project with groups, select a member node in either List or Graph to run it from its
details panel (or the command palette, Ctrl/Cmd-K). Assets and sensors use cache-aware get;
tasks use run. Groups organize nodes and are not run targets. Outside a group, the Run at the
end of each page’s toolbar runs what is open: Run billing.py gets that file’s assets and
sensors, and Run all gets every one after a confirmation; its tooltip gives the count
(Run all 8,070 assets, 1 sensor, which leaves out tasks, as they run only by name).
Read-only servers disable the actions.
The selected node shows as running as soon as the server accepts it; View run opens its
durable status, steps and logs. Ungrouped list panels offer the same action.
The full schemas are in specs/interfaces/server-api.openapi.yaml; every route is
described at https://barca.sh/reference/server-api/.
See also: barca docs scheduling, barca docs remote, barca docs contract.