Implementing Server Support For Fluree CLI
May 29, 2026 · View on GitHub
This document is for implementers building a custom server (for example in ../solo3/) that wants to support the Fluree CLI end-to-end.
The CLI supports two broad categories of remote operations:
- Data API: query / update / insert / upsert / info / exists / show / log / history / context / explain, plus admin operations like create / drop / reindex / branch (create / drop / rebase / merge) / publish / export.
- Replication / sync: clone / pull / fetch (content-addressed replication by CID, via pack + storage proxy) and ledger-archive (
export --format ledger).
Base URL And Discovery
The CLI prefers to be configured with a server origin URL (scheme/host/port) and then uses discovery:
GET /.well-known/fluree.jsonreturnsapi_base_url(usually/v1/fluree)
The CLI stores the discovered base as the remote's base_url and constructs all other endpoints relative to it.
If you do not implement discovery, users must configure the CLI remote URL to already include the API base (for example http://localhost:8090/v1), and the CLI will append /fluree as needed.
Minimum Endpoints By CLI Feature
fluree remote add, fluree auth login
GET /.well-known/fluree.json
fluree fetch (nameservice refs only)
GET {api_base_url}/nameservice/snapshotPOST {api_base_url}/nameservice/refs/:ledger-id/commitPOST {api_base_url}/nameservice/refs/:ledger-id/indexPOST {api_base_url}/nameservice/refs/:ledger-id/init
fluree clone, fluree pull (pack-first replication)
Required:
GET {api_base_url}/info/*ledger(existence + remotetpreflight; see/infominimum fields below)GET {api_base_url}/storage/ns/:ledger-id(remote NsRecord, includescommit_head_id, optionalindex_head_id, and optionalconfig_id)POST {api_base_url}/pack/*ledger(binaryfluree-pack-v1stream)
The CLI sends pack requests with index artifacts by default (include_indexes: true, want_index_root_id from the NsRecord) when the remote advertises an index_head_id. Use --no-indexes on clone/pull to request commits and txns only. Use --no-txns on clone to request commits without original transaction payloads (the commit chain still transfers and remains verifiable). Servers that support pack MUST honor the following request fields:
include_indexes: bool— whenfalse, skip index artifact frames.include_txns: bool— whenfalse, skip transaction blob frames. Commits are still streamed; the server must decode each commit's envelope and simply omit the referencedtxnblob from the stream. The emittedPackHeader.capabilitiesshould reflect this (drop"txns"from the list).
Servers that support pack should support all combinations of these flags.
Fallbacks (strongly recommended):
GET {api_base_url}/commits/*ledger(paginated export of commit + txn blobs)GET {api_base_url}/storage/objects/:cid?ledger=:ledger-id(per-object fetch by CID)
fluree push (commit ingestion)
POST {api_base_url}/push/*ledger
This is not storage-proxy replication; it is a transaction operation and should be authorized like normal transactions.
The CLI sends an Idempotency-Key header derived from the pushed commit bytes so servers can safely replay a successful push result if the client retries after a timeout.
fluree show --remote
GET {api_base_url}/show/*ledger?commit=<ref>
The commit query parameter accepts the same identifiers as the local fluree show command: t:<N> for transaction number, hex-digest prefix (min 6 chars), or full CID.
Policy filtering: The returned flakes are filtered by the caller's data-auth identity (extracted from the Bearer token) and the server's configured default_policy_class. When neither is present, all flakes are returned (root/admin access). Flakes the caller cannot read are silently omitted — the asserts and retracts counts reflect only the visible flakes. Unlike the query endpoints, show does not accept per-request policy overrides via headers or request body.
Response: A JSON object with fields: id, t, time, size, previous, signer, asserts, retracts, @context, flakes. Each flake is a tuple: [subject, predicate, object, datatype, operation].
Error responses:
400 Bad Request— missing or invalidcommitparameter404 Not Found— ledger or commit not found501 Not Implemented— proxy storage mode (no local index available for decoding)
fluree create <ledger> --remote <name> (admin-protected, empty ledger only)
POST {api_base_url}/createwith{"ledger": "<ledger>"}
Creates an empty ledger on the remote server. The CLI rejects --remote together with --from / --memory (those import paths require local data ingestion); the suggested workflow is to create + populate locally, then run fluree publish <remote> <ledger> which calls /exists, /create, and /push in sequence.
--remote does not touch local state — neither the active-ledger pointer nor the local storage tree. The CLI does not require a project-local .fluree/ for create --remote; it falls back to global config ($FLUREE_HOME or the platform default) for remote registration lookups. Auto-routing through a local server is not done for create; you must pass --remote <name> explicitly. Without --remote, fluree create is local-only and does require a project .fluree/.
fluree context get|set --remote
GET {api_base_url}/context/*ledger(read)PUT {api_base_url}/context/*ledger(write)
Read or replace the default JSON-LD context for a ledger. get returns the context as JSON; the unwrapped object is what the CLI prints. set accepts either a bare object ({"ex": "http://example.org/"}) or a {"@context": {...}} wrapper, and replies with {"status": "updated"} (or 409 Conflict after CAS retries).
get uses normal data-read auth (Bearer required when data_auth.mode == required, gates on can_read(ledger)). set uses normal write auth (can_write(ledger)). Auto-routing behaves the same way as other read/write commands — pass --direct to skip.
fluree history --remote
POST {api_base_url}/query/*ledger
Server-side history queries via JSON-LD: the CLI builds the same from/to/select/where body it would send locally and POSTs it to the ledger-scoped query endpoint (/query/{ledger}). The path carries the bare ledger ID (e.g. mydb:main) so the server's can_read check matches normal scoped read tokens; the body's from carries the time-travel suffix (mydb:main@t:N) which the query engine uses to build a historical view at that t. Posting to the connection-level /query instead would force auth to read from for the ledger ID and reject any token not scoped to the time-travel form.
Entity and predicate compact IRIs (ex:alice → http://example.org/alice) are expanded client-side using the project's stored prefix map before the request leaves the CLI, so the server never has to consult the local prefix table. The query body still ships its @context (also derived from local prefixes) so the server can compact response IRIs back into the user's preferred form for display.
fluree log --remote
GET {api_base_url}/log/*ledger?limit=<N>
Returns lightweight per-commit summaries newest-first by t. Read-auth (same bracket as /show) — does not require storage-replication permissions, unlike /commits. See Commit Log Contract for the response shape and required server semantics.
When --remote is omitted, the CLI auto-routes through a locally running fluree server start if one is detected; pass --direct to skip auto-routing and use the local commit-chain walker.
fluree export --remote (admin-protected)
POST {api_base_url}/export/*ledger
Returns ledger data as RDF in the requested format (Turtle, N-Triples, N-Quads, TriG, or JSON-LD). Admin-protected — same bracket as /create, /drop, /reindex. RDF export today reads from the binary index without per-flake policy filtering, which is why it does not live in the data-read bracket alongside /query and /show. See RDF Export Contract for the request body fields and content-type mapping.
When --remote is omitted, the CLI auto-routes through a locally running server when one is detected; pass --direct to bypass routing and use the local binary index. Tracked ledgers (no local data) require --remote.
fluree publish <remote> [ledger] (create + push)
Creates a ledger on the remote and pushes all local commits in a single operation.
Required endpoints:
GET {api_base_url}/exists/*ledger(check if ledger already exists)POST {api_base_url}/create(create empty ledger if not exists)GET {api_base_url}/info/*ledger(check remote head when ledger exists)POST {api_base_url}/push/*ledger(push all commits)
Workflow:
- CLI calls
GET /exists?ledger=mydb:main - If
exists: false, CLI callsPOST /createwith{"ledger": "mydb:main"} - If
exists: true, CLI callsGET /info/mydb:mainand rejects ift > 0(remote already has data) - CLI walks the full local commit chain (oldest → newest) and sends all commits via
POST /push/mydb:main - CLI configures upstream tracking locally
The --remote-name flag allows publishing under a different name on the remote (e.g., fluree publish origin mydb --remote-name production-db).
fluree drop <name> --remote <name> (admin-protected)
POST {api_base_url}/dropwith{"ledger": "<name>", "hard": true}
Drops a ledger or graph source on the remote server. The CLI sends hard: true (no soft-drop surface today). The server resolves name as a ledger first, then as a graph source — see the fluree drop graph source fallback section below for the resolution order and response shape.
When --remote is omitted, the CLI auto-routes through a locally running fluree server start if server.meta.json is present and the PID is alive, falling back to direct local execution otherwise. Pass --direct to skip auto-routing. The --force flag is required in all modes to confirm deletion.
Active-ledger handling:
--remote <name>(explicit): never touches local state. Remote storage is separate; the local active-ledger pointer and local storage are unaffected.- Auto-route (no
--remote, server running): same on-disk storage as--direct, so a successful drop also clears the local active-ledger pointer if it matched the dropped name. --direct(no--remote, no server): clears the active-ledger pointer if it matched.
fluree create <name> --from <file>.flpack (native ledger import)
- No server endpoint required (local-only operation)
Imports a .flpack file (native ledger pack) into a new local ledger. The .flpack format uses the same fluree-pack-v1 wire format as POST /pack. See Ledger portability below.
fluree export --format ledger
Exports a full ledger (all commits, txn blobs, and — unless --no-indexes — binary index artifacts) as a .flpack archive. The archive contains a phase: "nameservice" manifest frame so the importer can reconstruct the head pointers. Pass -o <FILE> to write to disk (required when stdout is a TTY).
Local mode (default):
- No server endpoint required.
Streams from the local ledger via the Fluree::archive_ledger API.
Remote mode (--remote <name>):
GET {api_base_url}/storage/ns/:ledger-id(NsRecord lookup)POST {api_base_url}/pack/*ledger(binaryfluree-pack-v1stream)
The CLI fetches the remote NsRecord to learn the head CIDs and t values, then streams the pack response into the user's writer, swapping the terminal End frame for a synthesized phase: "nameservice" manifest + End. The resulting .flpack is byte-compatible with a locally-generated archive — fluree create --from <file>.flpack doesn't care which side produced it.
Auth: Both endpoints sit in the replication-grade bracket and require a Bearer token with fluree.storage.* permissions (same auth as fluree clone/pull). Without those permissions the server returns 404 Not Found for /storage/ns/:ledger-id to avoid existence leaks; the CLI surfaces this as not found: ledger '...' not found on remote '...'.
See Ledger portability below for the on-disk format and Replication Auth Contract for the auth semantics.
fluree query, fluree insert, fluree upsert, fluree update, fluree track, fluree info, fluree exists
POST {api_base_url}/query/*ledgerPOST {api_base_url}/insert/*ledgerPOST {api_base_url}/upsert/*ledgerPOST {api_base_url}/update/*ledgerGET {api_base_url}/info/*ledgerGET {api_base_url}/exists/*ledger
When the CLI is invoked with policy flags (--as, --policy-class,
--policy, --policy-file, --policy-values, --policy-values-file,
--default-allow), it carries them on every data API request via the headers
listed below and, for JSON-LD bodies, also injects them into opts. To be
CLI-compatible, your server must implement the contract in
Policy Enforcement Contract.
Remote time travel (--at) routes through the ledger-scoped endpoints
(POST /query/{ledger}, etc.): the URL path drives the bearer's
can_read check (so a token scoped to mydb:main matches), and the
time-travel suffix rides in the body's from (mydb:main@t:N for JSON-LD)
or in an injected FROM <mydb:main@t:N> clause (for SPARQL). Posting to
the connection-level endpoint instead would force auth to derive the
ledger ID from from and reject scoped tokens.
Remote --at --explain flows through the same ledger-scoped path. The
CLI injects the time-travel suffix into from (JSON-LD) or as a FROM <ledger@t:N> clause (SPARQL), then POSTs to POST /explain/{ledger}.
The server's explain handlers route those requests through a
dataset-aware path so the request is processed against a view at the
requested t. Note that Fluree maintains one set of index stats
(latest), so explain plans for a given query text are largely
independent of t — the value of --at --explain is in honoring the
contract and consistency with the query path, not in producing
materially different plans.
fluree multi-query
POST {api_base_url}/multi-query
Bundles N JSON-LD and/or SPARQL sub-queries into a single envelope that
the server runs in parallel against one resolved snapshot moment. The
CLI reads the envelope JSON (file / stdin / -e inline) and POSTs it
to the connection-scoped /multi-query endpoint — each sub-query
declares its own from, so there is no ledger-scoped variant.
fluree multi-query resolves its transport in the same priority as fluree query:
--remote <name>— explicit; routes through the named remote's configuredbase_url. OIDC token refresh is persisted back toconfig.tomlafter a successful round-trip (same code pathfluree query --remoteuses via [context::persist_refreshed_tokens]).- Auto-route to a locally running
fluree server— used when--remoteis omitted andserver.meta.jsonreports a live pid; bypassed by--direct. No token persistence on this branch (the local server doesn't require auth). - In-process local — when neither of the above applies (no remote, no running server, or
--directwith no remote), the CLI callsFluree::multi_query()directly against the storage tree configured for this.fluree/directory. Same code path the server handler ultimately invokes; the only thing that changes is the boundary at which the request enters the api crate. No HTTP, no auth, no impersonation gate — the caller already has direct authority over the local storage.
Authentication uses the same MaybeCredential + MaybeDataBearer
extractor stack as /query — Bearer tokens (JWT/JWS) and signed
requests (JWS/VC) both work. Bearer ledger-scope is enforced on
every distinct ledger referenced in the envelope: any out-of-scope
ledger triggers a 404 on the whole envelope (existence-leak avoidance
matching /query's behavior), not a per-alias error.
Envelope-resident knobs replace some single-query CLI flags. Multi-query doesn't take --at (use envelope-level asOf) or --track-* / --max-fuel (use envelope-level opts.meta and per-sub-query opts.max-fuel). It does accept the full --policy* flag bundle (--as, --policy-class, --policy, --policy-file, --policy-values, --policy-values-file, --default-allow) — the same surface fluree query exposes. The headers ride through the transport identically; each sub-query carries its own from, so policy applies per-ledger via the standard server-side policy path. See Multi-query envelope for the full envelope contract, response shape, merge rules, bounds, and current limitations (history queries rejected, envelope max-fuel rejected, response cap enforced at assembly, SPARQL policy parity gap).
Output formatting uses two independent CLI flags:
--format json|typed-jsonselects the per-alias result shape (server-side formatter applied to each alias's entry insideresults). Mirrors the--formatflag onfluree query.--normalize-arrayswraps single-valued JSON-LD properties in arrays. Composes with--formaton JSON-LD aliases; on SPARQL aliases it is a no-op (SPARQL Results JSON has its own binding shape).--output json|pretty|aliasescontrols how the CLI prints the response envelope on the terminal; it doesn't affect alias results.
On the wire, --format / --normalize-arrays ride as Fluree-Output-Format / Fluree-Normalize-Arrays headers when going through --remote or auto-route; the in-process path wires them straight into the api crate's MultiQueryBuilder::format(...). The server reads them with precedence Fluree-Output-Format > Fluree-Normalize-Arrays alone > Accept-header content negotiation. Unknown Fluree-Output-Format values return 400 Bad Request; Accept values that produce byte/string payloads (TSV / CSV / SPARQL XML / RDF XML) return 406 Not Acceptable when no explicit Fluree-Output-Format is set. --format typed-json is cross-language (applied to every alias); --format json (the default) keeps SPARQL aliases on SPARQL Results JSON. See Multi-query envelope → Output formatting for the full table.
fluree branch list (read-only)
GET {api_base_url}/branch/{ledger}— note singularbranch, ledger is a greedy tail segment (*ledgerin axum), somydbandorg/mydbboth work.
Returns all non-retracted branches for the ledger. Same auth bracket as other
read endpoints (GET /branch/*ledger enforces Bearer when
data_auth.mode == required and can_read(ledger); returns 404 not 403
when the bearer cannot read it). See
Branch List Contract.
fluree branch create --remote <name> (admin-protected)
POST {api_base_url}/branchwith{ ledger, branch, source? }
Same admin auth bracket as /create, /drop, /reindex. See
Branch Create Contract.
fluree branch drop --remote <name> (admin-protected)
POST {api_base_url}/drop-branchwith{ ledger, branch }
Same admin auth bracket as /create, /drop, /reindex. See
Branch Drop Contract.
fluree graph drop --remote <name> (admin-protected)
POST {api_base_url}/drop-graphwith{ ledger, graph }
Drops a single named graph from one branch of a ledger by transactionally
retracting every triple currently asserted in it. History is preserved —
queries as-of an earlier t still see the graph populated. The graph IRI
remains registered so it can be re-populated by a later insert. Refuses the
default graph and the system txn-meta / config graphs. Same admin auth
bracket as /create, /drop, /reindex. See
Drop Named Graph Contract.
fluree graph list (read-only)
GET {api_base_url}/info/*ledger
Lists the user-defined named graphs registered on the targeted branch by
parsing the named-graphs section of the standard /info response. No
new endpoint is required. The CLI hides the default graph and the system
txn-meta / config graphs by default; --include-system surfaces them
alongside user graphs. See Graph List Contract.
fluree branch rebase --remote <name> (admin-protected)
POST {api_base_url}/rebasewith{ ledger, branch, strategy? }
Same admin auth bracket as /create, /drop, /reindex. See
Rebase Contract.
fluree branch merge --remote <name> (admin-protected)
POST {api_base_url}/mergewith{ ledger, source, target?, strategy? }
Same admin auth bracket as /create, /drop, /reindex. See
Merge Contract.
fluree branch diff (read-only merge preview)
GET {api_base_url}/merge-preview/*ledger?source=&target=&max_commits=&max_conflict_keys=&include_conflicts=
Returns the rich diff between two branches — ahead/behind commit summaries, common ancestor, conflict keys, fast-forward eligibility — without mutating any nameservice or content-store state. See Merge Preview Contract for the full semantic and response-shape spec.
Policy Enforcement Contract
CLI policy flags ride on every data API request as both HTTP headers and (for
JSON-LD bodies) body-level opts fields. Servers wanting full CLI parity must
honor both transports and apply the root-impersonation gate described
below.
Headers the CLI may send
| Header | CLI flag | Type | Notes |
|---|---|---|---|
fluree-identity | --as <iri> | string | Identity IRI to execute as. |
fluree-policy-class | --policy-class <iri> | string, repeatable | Send one header per class, OR a single header with comma-separated IRIs. Both forms must accumulate into a single list. |
fluree-policy | --policy <json> / --policy-file | JSON string | Inline JSON-LD policy document(s). Reject with 400 on parse failure. |
fluree-policy-values | --policy-values <json> / --policy-values-file | JSON object string | Variable bindings for parameterized policies (keys begin with ?$). Reject with 400 on parse failure or non-object value. |
fluree-default-allow | --default-allow | "true" (presence-truthy) | Permit access when no matching policy rules exist. |
For JSON-LD requests (POST /query/*, POST /insert/*, POST /upsert/*,
POST /update/* with Content-Type: application/json), the CLI also
injects each field into the request body's opts object using the same names
(opts.identity, opts.policy-class as a JSON array, opts.policy,
opts.policy-values as an object, opts.default-allow as a bool). Servers
should treat header values as defaults that body values override.
For SPARQL requests (Content-Type: application/sparql-query,
application/sparql-update), headers are the only transport — the SPARQL body
has no opts block.
For POST /multi-query, the CLI does not inject policy fields into the
envelope body — it sends headers only. The server folds the headers into the
envelope's top-level opts before validation (so envelope-level
rejections like max-fuel apply to header-supplied values too), and the
standard envelope → sub-query opts merge then carries them into every alias.
Per-language effect:
- JSON-LD sub-queries consume the merged
opts.identity/opts.policy-class/opts.policy/opts.policy-values/opts.default-allowviaapply_auth_identity_to_optsand the regular connection-scoped JSON-LD dispatch path — same code pathPOST /queryuses for single queries. - SPARQL sub-queries match the single-query connection-scoped SPARQL behaviour of
POST /querywithContent-Type: application/sparql-queryand an inlineFROM: bearer-scope reads apply, but identity threading viaQueryConnectionOptions(opts.identity,opts.policy-class, etc.) is not currently consumed by that path. The headers still ride through the transport, and the envelope-level fold still happens, but the SPARQL dispatcher (query_from().sparql()) does not act on policy opts. This gap is the same one documented for connection-scoped SPARQL today. See Multi-query envelope → Limitations for the canonical list.
Required server behavior
-
Build a
PolicyContextfrom the merged opts (header defaults + body overrides) and apply it to every query and transaction execution path. Without policy fields the request runs under root (no enforcement). With any policy field, the policies must be enforced — including for unsigned bearer-only transactions, which historically bypassed enforcement. -
Force the bearer's identity into
opts.identityby default (the bearer is the authenticated principal; clients cannot spoof identity by settingopts.identity). The exception is the impersonation gate below. -
Implement the impersonation gate for JSON-LD
opts.identity,opts.policy-class,opts.policy, andopts.policy-values, plus thefluree-identityheader on SPARQL requests:- Resolve the bearer's identity in the target ledger's policy graph.
- If the lookup returns "subject exists with no
f:policyClass" (theFoundNoPoliciesoutcome — the bearer is unrestricted on this ledger), respect the client-supplied identity / policy fields. - If the lookup returns "subject has
f:policyClassassignments" (FoundWithPolicies) or "subject not found" (NotFound), force the bearer identity intoopts.identityand ignore the client-supplied policy fields — the request runs under the bearer's own policies. opts.default-allowis not an impersonation field — it only governs the absence of matching rules and should not trigger the gate's lookup.
-
Audit-log impersonations. When the gate honors a client-supplied identity, log at
infolevel with the bearer, target, and ledger:policy impersonation: bearer=<bearer-id> target=<as-iri> ledger=<name> -
Set commit
authorto the impersonated identity for write operations. The original bearer is captured in the audit log; the commit's author field tracks who the operation was executed as. -
In proxy/forwarding mode, defer the gate to the upstream server: forward the request as-is and let the upstream resolve the gate against its own ledger state.
Reference behavior
The Fluree reference server implements the gate via
fluree_db_api::identity_has_no_policies(snapshot, overlay, t, identity_iri),
which wraps the three-state IdentityLookupResult enum and returns true
only for FoundNoPolicies. Source: fluree-db-api/src/policy_builder.rs.
The route-level wiring (header merge, gate, force-override, audit log,
PolicyContext construction) lives in
fluree-db-server/src/routes/policy_auth.rs — useful as a concrete
implementation reference if you're porting the contract to another server.
Tracking Contract
CLI tracking flags (--track, --track-fuel, --track-time,
--track-policy, --max-fuel) ride on every query request as HTTP headers.
A server that implements this contract makes the same flags Just Work
against the bundled Fluree server, a CLI-auto-routed local server, an
explicit --remote, and any custom HTTP implementation.
Request headers
| Header | CLI flag(s) | Type | Notes |
|---|---|---|---|
fluree-track-meta | --track | "true" (presence-truthy) | Shorthand: enable fuel + time + policy. |
fluree-track-fuel | --track-fuel (also implied by --max-fuel) | "true" | Report total fuel consumed. |
fluree-track-time | --track-time | "true" | Report query execution time. |
fluree-track-policy | --track-policy | "true" | Report per-policy executed/allowed counts. |
fluree-max-fuel | --max-fuel <N> | decimal string | Abort with 400 (or equivalent) when fuel exceeds N. Implies fuel tracking. |
The CLI only sends headers that map to enabled flags — a server should
treat each header as independent. fluree-track-meta is a shorthand that
the server may expand to all three; alternatively, when --track is set
the CLI may collapse to the single fluree-track-meta header for cleaner
wire format.
For JSON-LD requests, equivalent body opts exist (opts.meta,
opts.max-fuel); the CLI prefers headers so a single transport works
across JSON-LD and SPARQL. Servers should accept either.
Required server behavior
-
Inspect the headers (and body opts) and build a tracker before executing the query. Tracker construction is per-request — never reuse one across requests.
-
Enforce
fluree-max-fuelstrictly: abort the query as soon as accumulated fuel would exceed the limit and return an error response. The reference server returns400 Bad Requestwith a body describing the limit and the amount used. -
Return a
TrackedQueryResponse-shaped body when any tracking header is present:{ "status": 200, "result": <the normal query result body>, "time": "12.34ms", "fuel": 1234.567, "policy": { "<policy-id>": { "executed": 3, "allowed": 2 } } }Only include
time,fuel,policyfor metrics the client actually requested. Theresultfield carries whatever the untracked response body would have been (SPARQL JSON, JSON-LD, agent-json, etc.). For agent-json responses the server SHOULD return the bare agent-json envelope as the response body and surface the tally only via the response headers below, so agents see the same shape they always do. -
Echo the tally on response headers so callers that don't parse the JSON body (e.g. delimited or binary formats) can still read them:
Response header Source Format x-fdb-fueltracker.fuel decimal string x-fdb-timetracker.time duration string, e.g. "12.34ms"x-fdb-policytracker.policy JSON object
Reference behavior
The reference server's per-route wiring lives in
fluree-db-server/src/routes/query.rs (see the has_tracking() branch
on the ledger-scoped and connection-scoped query handlers). The tracker
implementation, micro-fuel internals (1 fuel = 1000 micro-fuel), and the
TrackedQueryResponse / PolicyStats shapes are defined in
fluree-db-core/src/tracking.rs. The full fuel cost ladder for queries
and transactions is in docs/query/tracking-and-fuel.md.
Merge Preview Contract
fluree branch diff issues a single read-only request:
GET {api_base_url}/merge-preview/{ledger}?source={source}&target={target}
&max_commits={n}&max_conflict_keys={n}&include_conflicts={bool}
&include_conflict_details={bool}&strategy={strategy}
| Parameter | Type | Required | Server default | Description |
|---|---|---|---|---|
ledger (path) | string | Yes | — | Ledger name without branch suffix |
source | string | Yes | — | Source branch to merge from |
target | string | No | source's parent branch | Target branch to merge into |
max_commits | integer | No | 500 | Per-side cap on ahead.commits / behind.commits |
max_conflict_keys | integer | No | 200 | Cap on conflicts.keys |
include_conflicts | bool | No | true | When false, the conflict computation is skipped |
include_conflict_details | bool | No | false | When true, include source/target flake values for the returned conflict keys |
strategy | string | No | take-both | Strategy used for resolution labels in conflicts.details[].resolution; one of take-both, abort, take-source, take-branch |
Auth follows the same pattern as GET /branch/*ledger (read-only): require
a Bearer when data_auth.mode == required; gate on can_read(ledger);
return 404 (not 403) when the bearer cannot read it.
Required semantics
These rules are not negotiable; the CLI and other clients depend on them:
- Source resolution.
sourcemust be a branch — its nameservice record must havesource_branch != null. Otherwise respond400with a message containing"no source branch"so the CLI's error matcher works. - Target defaulting. When
targetis omitted, resolve tosource.source_branch. - Self-merge. If
source == resolved_target, respond400with a message containing"itself". - Cross-branch ancestor lookup.
ancestoris the most recent common commit betweensourceHEAD andtargetHEAD. The walk must be able to load commit envelopes from both branches' namespaces — sibling branches offmainmust work. The reference implementation builds a union view that fans out through bothBranchedContentStoreancestries; equivalents are fine. - Fast-forward predicate.
fast_forward = (ancestor.commit_id == target_head)when both heads exist;truewhen both heads are absent;falseotherwise. - Per-side walks.
ahead.countis the total number of commits onsourcesinceancestor.t(uncapped).ahead.commitsis the same set, capped atmax_commits, strictly newest-first byt.truncated = count > commits.len(). Same shape forbehind. - Conflict computation. When
include_conflicts == true && !fast_forwardand both heads exist:- Walk both deltas:
(s, p, g)tuples touched on each side sinceancestor.t. conflicts.keysis the intersection.- Sort the intersection before truncating —
HashSet::intersectionorder is unspecified, and stable ordering matters for paginated UIs. Lexicographic by(s, p, g)is fine; what matters is that two requests against the same state return the same prefix. countis the unbounded intersection size;truncated = count > cap.
- Walk both deltas:
- Conflict details. When
include_conflict_details == true, populateconflicts.detailsfor the keys returned inconflicts.keysafter truncation. Each detail includeskey,source_values,target_values, and aresolutionannotation for the requestedstrategy. The values are the current asserted values for that key at each branch HEAD; preview must not apply the strategy. Use the same resolved flake tuple shape as/show([s, p, o, dt, op], optional metadata as a 6th item). - No mutations. Implementations must not write to the nameservice, advance any HEAD, copy commits between namespaces, or update any cache that downstream operations depend on.
- Server-side cap is mandatory. Even if a client sends
max_commits=10000000, clamp to a defensive limit. The reference server applies two layers: when no query param is present, it falls back to the recommended defaults (500for commits,200for conflict keys); when a param is present, the server clamps the caller's value withmin(value, hard_max)where the reference hard maxes are5_000for commits and5_000for conflict keys (constantsMERGE_PREVIEW_HARD_MAX_COMMITSandMERGE_PREVIEW_HARD_MAX_CONFLICT_KEYSinfluree-db-server/src/routes/ledger.rs). The CLI assumes the server enforces a cap, and unbounded responses must not be reachable over HTTP regardless of what the client requests.
Scope of the cap. This bounds the size of the returned lists
and the per-summary load_commit_by_id reads (one full commit blob
per summary). It does not bound the underlying divergence walk:
count on each side reflects the unbounded divergence and is computed
by walking every commit envelope between HEAD and the ancestor.
Likewise, conflict computation walks the full per-side delta when
include_conflicts=true. If you need to refuse expensive previews,
add a separate operational guard before invoking the walk (for
example, reject when target.t - ancestor.t exceeds some threshold)
or document that clients should pass include_conflicts=false for a
cheaper preview.
Response (200 OK)
{
"source": "feature-x",
"target": "main",
"ancestor": { "commit_id": "bafy...", "t": 5 },
"ahead": {
"count": 3,
"commits": [
{
"t": 8,
"commit_id": "bafy...",
"time": "2026-04-25T12:00:00Z",
"asserts": 2,
"retracts": 0,
"flake_count": 2,
"message": null
}
// ... newest-first
],
"truncated": false
},
"behind": { "count": 1, "commits": [], "truncated": false },
"fast_forward": false,
"mergeable": true,
"conflicts": {
"count": 1,
"keys": [{ "s": [100, "alice"], "p": [100, "status"], "g": null }],
"truncated": false,
"strategy": "take-source",
"details": [
{
"key": { "s": [100, "alice"], "p": [100, "status"], "g": null },
"source_values": [["ex:alice", "ex:status", "active", "xsd:string", true]],
"target_values": [["ex:alice", "ex:status", "archived", "xsd:string", true]],
"resolution": {
"source_action": "kept",
"target_action": "retracted",
"outcome": "source-wins"
}
}
]
}
}
ancestor is null only when both heads are absent. Each CommitSummary
sets time to null for legacy commits without a timestamp; message is
extracted from txn_meta when an entry with predicate f:message (Fluree
DB system namespace, local name "message") and a string value is present.
Other conventions are not recognized — return null.
ConflictKey encodes a (s, p, g) tuple. The wire shape mirrors
fluree_db_core::ConflictKey:
{
"s": [<namespace_code: u16>, "<local_name>"],
"p": [<namespace_code: u16>, "<local_name>"],
"g": [<namespace_code: u16>, "<local_name>"] // or null for the default graph
}
Sids serialize as [ns_code, name] tuples. Changing the encoding will
break the CLI.
When include_conflict_details=false, conflicts.details is omitted. When it
is true, source_values and target_values are resolved flake tuples for the
current asserted values in the same shape returned by GET /show/*ledger;
resolution is a label only. mergeable is false when the chosen strategy
would abort (currently strategy=abort with one or more conflicts). It is not
full transaction validation for constraints that might fail during the real
merge commit. mergeable=true does not guarantee a subsequent POST /merge
will succeed; it only reflects the conflict/strategy interaction at preview
time.
Error responses
| Status | When |
|---|---|
400 | Source has no parent (e.g., main); source == target; unknown strategy; unsupported strategy; include_conflict_details=true with include_conflicts=false; strategy=abort with include_conflicts=false. Body must include "no source branch" or "itself" for the first two cases so the CLI's matcher works. |
401 | Bearer required and absent/invalid. |
404 | Ledger or branch does not exist; or the bearer cannot can_read. |
5xx | Storage / nameservice errors. |
Reference implementation
| Concern | Canonical location |
|---|---|
| HTTP route + auth | fluree-db-server/src/routes/ledger.rs::merge_preview |
| Orchestration | fluree-db-api/src/merge_preview.rs::merge_preview_with |
| Per-commit summary + DAG walk | fluree-db-core/src/commit.rs::walk_commit_summaries |
| Common ancestor (dual-frontier BFS) | fluree-db-core/src/commit.rs::find_common_ancestor |
| Delta-key computation | fluree-db-novelty/src/delta.rs::compute_delta_keys |
Validate compatibility by running fluree branch diff dev --target feature --remote your-remote --json against your server and diffing the response
against output from the reference server on the same ledger state.
Branch List Contract
fluree branch list <ledger> --remote <name> issues:
GET {api_base_url}/branch/{ledger}
The path segment is singular branch (not branches) and uses axum's
greedy *ledger tail capture, so a ledger named org/mydb is matched by
/branch/org/mydb. The endpoint takes no query parameters and no body.
Auth
Read-only. Requires a Bearer when data_auth.mode == required; gates on
can_read(ledger); returns 404 (not 403) when the bearer cannot read it
to avoid existence leaks. Admin tokens are NOT required.
Response (200 OK)
A JSON array of BranchInfo. Empty array when the ledger has no
non-retracted branches.
[
{
"branch": "main",
"ledger_id": "mydb:main",
"t": 12,
"source": null
},
{
"branch": "feature-x",
"ledger_id": "mydb:feature-x",
"t": 15,
"source": "main"
}
]
| Field | Type | Notes |
|---|---|---|
branch | string | Branch name. |
ledger_id | string | Full ledger:branch identifier. |
t | integer | Current commit t on this branch. |
source | string | null | Parent branch, or null for root branches like main. Omitted via skip_serializing_if = "Option::is_none" when null. |
Error responses
| Status | When |
|---|---|
401 | Bearer required and absent/invalid. |
404 | Ledger does not exist; or the bearer cannot can_read. |
5xx | Storage / nameservice errors. |
Reference implementation
| Concern | Canonical location |
|---|---|
| HTTP route + auth | fluree-db-server/src/routes/ledger.rs::list_branches |
| Response shape | fluree-db-server/src/routes/ledger.rs::BranchInfo |
| Underlying API | fluree_db_api::Fluree::list_branches |
Branch Create Contract
fluree branch create <name> --remote <name> issues:
POST {api_base_url}/branch
Content-Type: application/json
{
"ledger": "mydb",
"branch": "feature-x",
"source": "main"
}
The body type mirrors fluree-db-server::routes::ledger::CreateBranchRequest.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
ledger | string | Yes | — | Ledger name without branch suffix. |
branch | string | Yes | — | New branch name. Must pass validate_branch_name. |
source | string | No | "main" | Parent branch to fork from. The source must already exist and have at least one commit. |
Auth
Admin-protected. Same middleware as POST /create, POST /drop,
POST /reindex, and POST /iceberg/map — registered through
v1_admin_protected_routes in fluree-db-server/src/routes/mod.rs.
Response (201 Created)
{
"ledger_id": "mydb:feature-x",
"branch": "feature-x",
"source": "main",
"t": 12
}
| Field | Type | Notes |
|---|---|---|
ledger_id | string | Full ledger:branch identifier of the new branch. |
branch | string | New branch name (echoed). |
source | string | Resolved parent branch. Empty string if the new record's source_branch is unexpectedly null. |
t | integer | Commit t at the branch point (inherited from the source's HEAD). |
The CLI's pretty-printer (print_branch_created in
fluree-db-cli/src/commands/branch.rs) reads branch, source, t, and
ledger_id from the response — keep all four populated.
Error responses
| Status | When |
|---|---|
400 | Invalid branch name (per validate_branch_name); malformed JSON body. |
401 / 403 | Admin token required and absent/invalid (see admin-auth middleware). |
404 | Source branch does not exist. |
409 | A branch with this name already exists (ApiError::LedgerExists → 409). |
5xx | Nameservice / storage / index-copy errors. |
Reference implementation
| Concern | Canonical location |
|---|---|
| HTTP route + auth | fluree-db-server/src/routes/ledger.rs::create_branch |
| Request / response shapes | CreateBranchRequest, CreateBranchResponse (same file) |
| Underlying API | fluree_db_api::Fluree::create_branch (fluree-db-api/src/ledger/loading.rs) |
Branch Drop Contract
fluree branch drop <name> --remote <name> issues:
POST {api_base_url}/drop-branch
Content-Type: application/json
{
"ledger": "mydb",
"branch": "feature-x"
}
Note the endpoint is /drop-branch (hyphenated) — separate from the
ledger-level POST /drop endpoint.
| Field | Type | Required | Description |
|---|---|---|---|
ledger | string | Yes | Ledger name without branch suffix. |
branch | string | Yes | Branch to drop. Cannot be the root branch (any branch with source_branch.is_none()). "main" carries no special meaning — it's the default and so the root on most ledgers, but a ledger created with a different initial branch will refuse that branch instead, and a non-root branch named "main" is droppable. Use POST /drop to remove the whole ledger including its root. |
Auth
Admin-protected (same bracket as /branch, /rebase, /merge,
/create, /drop, /reindex).
Behavior
The reference server's Fluree::drop_branch:
- Refuses the root branch with
400(structural check onsource_branch.is_none(), not a name comparison). - If the branch is retracted already → returns status
already_retracted. - If the branch has children (
branches > 0) → soft-retracts it (preserves storage so children can still resolve), returnsdeferred: true. - If the branch is a leaf → cancels indexing, deletes all storage artifacts (commits, txns, index roots, leaves, branches, dicts, garbage records, config, context), purges the nameservice record, and cascades upward to any retracted ancestors that now have zero children.
Response (200 OK)
{
"ledger_id": "mydb:feature-x",
"status": "dropped",
"deferred": false,
"files_deleted": 14,
"cascaded": ["mydb:retired-parent"],
"warnings": []
}
| Field | Type | Notes |
|---|---|---|
ledger_id | string | Full ledger:branch identifier of the dropped branch. |
status | string | "dropped", "already_retracted", or "not_found". |
deferred | bool | true when the branch was retracted but storage preserved (had children). |
files_deleted | integer | Omitted when 0. |
cascaded | string[] | Ancestor ledger_ids that were cascade-dropped because they were retracted with zero remaining children. Omitted when empty. |
warnings | string[] | Non-fatal warnings (e.g. partial artifact deletion). Omitted when empty. |
The CLI's print_branch_dropped reads ledger_id, deferred,
files_deleted, cascaded, and warnings — populate them all.
Error responses
| Status | When |
|---|---|
400 | Attempting to drop the root branch (source_branch.is_none()); malformed JSON body. |
401 / 403 | Admin token required and absent/invalid. |
404 | Branch not found (the underlying lookup miss surfaces as ApiError::NotFound → 404). |
5xx | Storage / nameservice errors during purge. |
Reference implementation
| Concern | Canonical location |
|---|---|
| HTTP route + auth | fluree-db-server/src/routes/ledger.rs::drop_branch |
| Request / response shapes | DropBranchRequest, DropBranchResponse (same file) |
| Underlying API | fluree_db_api::Fluree::drop_branch (fluree-db-api/src/admin.rs) |
| Report struct | fluree_db_api::BranchDropReport |
Graph List Contract
fluree graph list --ledger <ledger> [--remote <name>] [--include-system] [--json] does not call a dedicated endpoint. It reuses:
GET {api_base_url}/info/*ledger
and parses the named-graphs array out of the response. Servers that already implement the /info Response Contract below are compatible with fluree graph list as long as they populate that array.
Auth
Read-only. Same auth bracket as GET /info/*ledger.
Required response field
The CLI requires info.ledger.named-graphs (or top-level named-graphs for older response shapes) to be a JSON array. Each entry must be a JSON object with at least:
| Field | Type | Notes |
|---|---|---|
iri | string | Full IRI of the named graph. Use "urn:default" for the default graph slot (g-id = 0) so the CLI can recognize it and filter it under --include-system. |
g-id | integer | Stable graph identifier in the registry. 0 = default, 1 = urn:fluree:{ledger_id}#txn-meta, 2 = urn:fluree:{ledger_id}#config, >= 3 = user-defined graphs. |
flakes | integer | Number of currently-asserted flakes in this graph at the response's t. May be 0. |
size | integer | On-disk size in bytes attributed to this graph in the binary index. May be 0. |
Additional fields are allowed and ignored by the CLI. Returning a stable order is recommended for paginated UIs but not required by the CLI.
CLI filtering
The CLI's table and --json output both apply the same filter:
- By default, entries with
g-id ∈ {0, 1, 2}oriri == "urn:default"oririequal to the ledger'stxn-meta/configIRI are omitted. --include-systemshows all four kinds (default,txn-meta,config, user).
The table view computes a Kind column from g-id plus the well-known IRI helpers in fluree_db_core::graph_registry.
Error responses
| Status | When |
|---|---|
401 | Bearer required and absent/invalid (same as /info). |
404 | Ledger does not exist. |
5xx | Storage / nameservice errors. |
A response that omits named-graphs is treated by the CLI as an outdated server; it prints a clear "info response is missing named-graphs" usage error rather than silently showing an empty list.
Reference implementation
| Concern | Canonical location |
|---|---|
| HTTP route | fluree-db-server/src/routes/ledger.rs::info_ledger_tail |
| Payload assembly | fluree-db-api/src/ledger_info.rs::build_ledger_info (populates named-graphs) |
| CLI dispatcher | fluree-db-cli/src/commands/graph.rs::run_list |
| System-graph constants | fluree_db_core::graph_registry::{DEFAULT_GRAPH_ID, TXN_META_GRAPH_ID, CONFIG_GRAPH_ID, txn_meta_graph_iri, config_graph_iri} |
Drop Named Graph Contract
fluree graph drop <graph-iri> --ledger <ledger> --remote <name> issues:
POST {api_base_url}/drop-graph
Content-Type: application/json
{
"ledger": "mydb:main",
"graph": "urn:example:org/payroll"
}
Note the endpoint is /drop-graph (hyphenated) — separate from the
ledger-level POST /drop and branch-level POST /drop-branch endpoints.
| Field | Type | Required | Description |
|---|---|---|---|
ledger | string | Yes | Full ledger identifier. A bare ledger name ("mydb") is normalized to "mydb:main". To target a branch other than the root, use the explicit "mydb:feature-x" form. |
graph | string | Yes | Full absolute IRI of the named graph to drop. Must have a valid <scheme>:<rest> head per RFC 3986 §3.1 and contain none of the characters RFC 3987 excludes from an IRI (whitespace, <, >, ", {, }, |, \, ^, `). Relative references (e.g. payroll) and leading/trailing whitespace are rejected — not trimmed. |
Auth
Admin-protected (same bracket as /create, /drop, /drop-branch,
/branch, /rebase, /merge, /reindex).
Behavior
The reference server's Fluree::drop_named_graph:
- Normalizes the ledger id (
:maindefault). - Validates the graph IRI as an absolute IRI (
<scheme>:<rest>with no whitespace or RFC 3987-excluded characters), then rejects the system graphs:- the default graph (empty /
g_id == 0), - the
txn-metagraph (urn:fluree:{ledger_id}#txn-meta,g_id == 1), - the
configgraph (urn:fluree:{ledger_id}#config,g_id == 2).
- the default graph (empty /
- Resolves
graphagainst the snapshot'sGraphRegistry. An unknown IRI returns404rather than silently registering a new graph slot. - Stages and commits a SPARQL UPDATE equivalent to
DELETE { GRAPH <iri> { ?s ?p ?o } } WHERE { GRAPH <iri> { ?s ?p ?o } }through the same pipeline used by user updates. This produces one new commit att = current_t + 1whose flakes are retractions only. - Reports a no-op (
committed: false,retracted: 0) when the graph was already empty — no new commit is created.
Key properties:
- History preserving. A query
as-ofan earliertstill sees every triple that was previously asserted in the graph. - Per-branch scope. Drops only affect the branch in
ledger. Sibling branches that share the same graph IRI are not touched. - Registry stable. The graph IRI keeps its
g_id; a subsequent insert into the same IRI lands in the same logical graph rather than a new slot.
Response (200 OK)
{
"ledger_id": "mydb:main",
"graph_iri": "urn:example:org/payroll",
"retracted": 42,
"committed": true,
"t": 18
}
| Field | Type | Notes |
|---|---|---|
ledger_id | string | Normalized ledger:branch identifier the drop targeted. |
graph_iri | string | Graph IRI that was dropped (echoed). |
retracted | integer | Number of flakes retracted by the drop commit. 0 when the graph was already empty. |
committed | bool | true when a new commit was produced; false for a no-op drop on an empty graph. |
t | integer | Current commit t for the branch after the drop. Equal to the pre-drop t when committed is false. |
The CLI's fluree graph drop printer (commands/graph.rs) reads
ledger_id, graph_iri, retracted, committed, and t — populate
them all.
Error responses
| Status | When |
|---|---|
400 | graph is empty, has whitespace or any IRI-excluded character, lacks a <scheme>:<rest> head (relative reference), or names a system graph (default, txn-meta, config); malformed JSON body. |
401 / 403 | Admin token required and absent/invalid. |
404 | Ledger does not exist; or graph is not registered in the ledger's graph registry. |
5xx | Storage / nameservice / commit-write errors during the retract commit. |
Reference implementation
| Concern | Canonical location |
|---|---|
| HTTP route + auth | fluree-db-server/src/routes/ledger.rs::drop_named_graph |
| Request / response shapes | DropNamedGraphRequest, DropNamedGraphResponse (same file) |
| Underlying API | fluree_db_api::Fluree::drop_named_graph (fluree-db-api/src/admin.rs) |
| Absolute-IRI validator | validate_absolute_iri (same file) |
| Report struct | fluree_db_api::DropNamedGraphReport |
| Graph registry | fluree_db_core::graph_registry (system graph constants and IRI helpers) |
Rebase Contract
fluree branch rebase <branch> --remote <name> issues:
POST {api_base_url}/rebase
Content-Type: application/json
{
"ledger": "mydb",
"branch": "feature-x",
"strategy": "take-both"
}
| Field | Type | Required | Server default | Description |
|---|---|---|---|---|
ledger | string | Yes | — | Ledger name without branch suffix. |
branch | string | Yes | — | Branch to rebase. Cannot be the root branch (source_branch.is_none()) — it has no parent to rebase onto. The check is structural, not name-based. |
strategy | string | No | "take-both" | One of take-both, abort, take-source, take-branch, skip. Parsed by ConflictStrategy::from_str_name; unknown values respond 400. |
Auth
Admin-protected (same bracket as /branch, /drop-branch, /merge,
/create, /drop, /reindex).
Behavior
Replays the branch's unique commits on top of its source branch's current
HEAD, detecting and resolving conflicts according to strategy. The branch's
own source_branch (from its nameservice record) is the rebase target — there
is no target field in the request.
- If the branch is already up-to-date with its source (
branch_head == ancestor), the operation is a fast-forward: the branch's HEAD is advanced to the source HEAD with no replay, andfast_forward: trueis returned. - If
strategy == "abort"and any branch commit conflicts with the source delta, the rebase aborts up-front with409 BranchConflict. No commits are written. - Otherwise, the branch's commits are replayed sequentially on top of the source HEAD using the chosen strategy for conflict resolution.
Response (200 OK)
{
"ledger_id": "mydb:feature-x",
"branch": "feature-x",
"fast_forward": false,
"replayed": 3,
"skipped": 0,
"conflicts": 1,
"failures": 0,
"total_commits": 3,
"source_head_t": 18
}
| Field | Type | Notes |
|---|---|---|
ledger_id | string | Full ledger:branch identifier of the rebased branch. |
branch | string | Branch name (echoed). |
fast_forward | bool | true when the branch had no unique commits and was just advanced. |
replayed | integer | Commits successfully replayed onto source HEAD. |
skipped | integer | Commits skipped (e.g. via skip strategy on conflicts). |
conflicts | integer | Total commits that contained conflicts. Note this is a count, not a list — the underlying RebaseReport carries Vec<RebaseConflict> and Vec<RebaseFailure>, but the HTTP response surfaces only the lengths. |
failures | integer | Commits that failed to replay (transactional / validation errors). |
total_commits | integer | Total branch commits considered for replay. |
source_head_t | integer | Source branch HEAD t after rebase. |
The CLI's print_rebase_result reads fast_forward, branch, source_head_t,
replayed, skipped, conflicts, and failures.
Error responses
| Status | When |
|---|---|
400 | Rebasing the root branch (no source_branch — surfaced as InvalidBranch); unknown / unsupported strategy; malformed JSON body. |
401 / 403 | Admin token required and absent/invalid. |
404 | Branch or its source not found. |
409 | BranchConflict — currently raised when strategy=abort and any commit conflicts with the source delta. |
5xx | Storage / nameservice / index-build errors during replay. |
Reference implementation
| Concern | Canonical location |
|---|---|
| HTTP route + auth | fluree-db-server/src/routes/ledger.rs::rebase |
| Request / response shapes | RebaseBranchRequest, RebaseBranchResponse (same file) |
| Underlying API | fluree_db_api::Fluree::rebase_branch (fluree-db-api/src/rebase.rs) |
| Report struct | fluree_db_api::RebaseReport |
| Strategy enum | fluree_db_api::ConflictStrategy |
Merge Contract
fluree branch merge <source> --remote <name> issues:
POST {api_base_url}/merge
Content-Type: application/json
{
"ledger": "mydb",
"source": "feature-x",
"target": "main",
"strategy": "take-both"
}
| Field | Type | Required | Server default | Description |
|---|---|---|---|---|
ledger | string | Yes | — | Ledger name without branch suffix. |
source | string | Yes | — | Branch to merge from. Must have at least one commit and a source_branch. |
target | string | No | source.source_branch | Branch to merge into. Defaults to the source's parent branch. Must not equal source. |
strategy | string | No | "take-both" | One of take-both, abort, take-source, take-branch. Parsed by ConflictStrategy::from_str_name. |
Auth
Admin-protected (same bracket as /branch, /drop-branch, /rebase,
/create, /drop, /reindex).
Behavior
- Computes the common ancestor between
sourceHEAD andtargetHEAD using aBranchedContentStoreso sibling branches offmainwork. - If
targetHEAD == ancestor, performs a fast-forward merge: copies the source's unique commit blobs into the target's namespace and advances the target HEAD. No conflict resolution runs.fast_forward: trueis reported. - Otherwise, performs a general merge: stages the union of source and
target deltas, resolves overlapping
(s, p, g)keys viastrategy, and writes a single new commit on the target.fast_forward: falseis reported. Ifstrategy == "abort"and conflicts exist, the merge fails with409 BranchConflictand the target is rolled back to its pre-merge nameservice snapshot.
Response (200 OK)
{
"ledger_id": "mydb:main",
"target": "main",
"source": "feature-x",
"fast_forward": false,
"new_head_t": 22,
"commits_copied": 4,
"conflict_count": 1,
"strategy": "take-both"
}
| Field | Type | Notes |
|---|---|---|
ledger_id | string | Full ledger:branch identifier of the target after merge. |
target | string | Resolved target branch (echoed; reflects the default if the request omitted it). |
source | string | Source branch name (echoed). |
fast_forward | bool | true for a fast-forward merge. |
new_head_t | integer | New commit t of the target after merge. |
commits_copied | integer | Number of commit blobs copied into the target's namespace. For fast-forward this equals the source's unique commits; for general merge this includes the synthesized merge commit. |
conflict_count | integer | Number of conflicts resolved. 0 for fast-forward. |
strategy | string | omitted | Strategy used. Omitted (via skip_serializing_if) for fast-forward merges where strategy doesn't apply. |
The CLI's print_merge_result reads source, target, new_head_t,
commits_copied, fast_forward, and conflict_count.
Error responses
| Status | When |
|---|---|
400 | Source has no source_branch (a root branch like main cannot be the source); source == resolved_target; source has no commits; unknown / unsupported strategy; malformed JSON body. |
401 / 403 | Admin token required and absent/invalid. |
404 | Source or target branch not found. |
409 | BranchConflict — currently raised when strategy=abort and conflicts exist. |
5xx | Storage / nameservice / commit-write errors. |
Reference implementation
| Concern | Canonical location |
|---|---|
| HTTP route + auth | fluree-db-server/src/routes/ledger.rs::merge |
| Request / response shapes | MergeBranchRequest, MergeBranchResponse (same file) |
| Underlying API | fluree_db_api::Fluree::merge_branch (fluree-db-api/src/merge.rs) |
| Report struct | fluree_db_api::MergeReport |
| Strategy enum | fluree_db_api::ConflictStrategy |
Replication Auth Contract
Replication endpoints are intentionally protected more strictly than data reads:
- Pack + commit export + storage proxy endpoints require a Bearer token with
fluree.storage.*permissions. - Unauthorized requests should return
404 Not Found(no existence leak) for these endpoints.
Data API endpoints use normal read/transaction auth (fluree.ledger.read.*, fluree.ledger.write.*) and should return 401/403/404 as appropriate for your product.
Pack Protocol Contract
- Endpoint:
POST {api_base_url}/pack/*ledger - Request: JSON
PackRequestwith"protocol":"fluree-pack-v1". Includesinclude_indexes: bool(defaulttruefor clone/pull;falsewith--no-indexes),include_txns: bool(defaulttrue;falsewith--no-txnson clone), and optionalwant_index_root_id/have_index_root_idwhen the CLI requests index data. - Response:
Content-Type: application/x-fluree-pack, streaming frames:- Preamble
FPK1+ version byte - Header frame (mandatory, first)
- Data frames: CID binary + raw object bytes
- Optional Manifest frames (phase transitions)
- End frame (mandatory termination)
- Preamble
Clients verify integrity:
- Commit-v2 blobs (
FCV2magic): sub-range hash verification. - All other objects: full-bytes hash verification by CID.
Graceful fallback: If you do not implement pack yet, return 404 Not Found, 405 Method Not Allowed, 406 Not Acceptable, or 501 Not Implemented. The CLI treats those as "pack not supported" and falls back to GET /commits plus GET /storage/objects/:cid.
Storage Proxy Contract
These endpoints exist so a client can fetch bytes by CID without knowing storage layout:
GET {api_base_url}/storage/ns/:ledger-idreturnsNsRecordJSON with CID identity fields:commit_head_id,commit_t,index_head_id,index_t, optionalconfig_id
GET {api_base_url}/storage/objects/:cid?ledger=:ledger-idreturns raw bytes for the CID after verifying integrity.
/storage/block is only required for query peers that need server-mediated index-leaf access.
Commit Log Contract
fluree log --remote issues a single read-only request:
GET {api_base_url}/log/{ledger}?limit={n}
| Parameter | Type | Required | Server default | Description |
|---|---|---|---|---|
ledger (path) | string | Yes | — | Ledger ID, including branch suffix (org/mydb and org/mydb:main both work via the greedy *ledger capture) |
limit | integer | No | 100 | Number of summaries to return (newest-first by t). Server clamps to a hard maximum (reference: 5000). |
Auth
Read-only. Requires a Bearer token when data_auth.mode == required; gates on
can_read(ledger); returns 404 (not 403) when the bearer cannot read the
ledger so it doesn't leak existence. Admin tokens are NOT required.
Response (200 OK)
{
"ledger_id": "mydb:main",
"commits": [
{
"t": 12,
"commit_id": "bafy...",
"time": "2026-04-25T12:00:00Z",
"asserts": 3,
"retracts": 0,
"flake_count": 3,
"message": null
}
// ... newest-first by t
],
"count": 12,
"truncated": false
}
| Field | Type | Notes |
|---|---|---|
ledger_id | string | Ledger ID echoed from the request path. |
commits | array | Per-commit summaries, strictly newest-first by t, capped at the resolved limit. |
count | integer | Total commits in the chain (uncapped). truncated == count > commits.len(). |
truncated | bool | true when the chain is longer than the returned page. |
Each commits[i] mirrors fluree_db_core::CommitSummary:
| Field | Type | Notes |
|---|---|---|
t | integer | Transaction number. |
commit_id | string | Content ID (CID) of the commit blob. |
time | string | null | ISO-8601 commit time, or null for legacy commits without a timestamp. |
asserts | integer | Asserted flakes in this commit. |
retracts | integer | Retracted flakes. |
flake_count | integer | Total flakes (asserts + retracts). |
message | string | null | Extracted from txn_meta when an f:message entry with a string value is present. Returns null otherwise. |
Required semantics
- Branch-aware walk. The walk must load commit envelopes via a
branch-aware content store (the reference server uses
branched_content_store_for_record). Pre-fork commits live under the source branch's namespace, so a flat per-branch store cannot reach them and the response would be incomplete. - Newest-first ordering.
commitsis sorted strictly descending byt. The CLI prints in this order without re-sorting. - Empty ledger. When the ledger exists but has no commits, return
200 OKwithcommits: []andcount: 0. - Hard cap. Servers MUST enforce a hard maximum independent of the
client's
limit(reference:5000). The CLI assumes the server caps the response, and unbounded responses must not be reachable.
Error responses
| Status | When |
|---|---|
401 | Bearer required and absent/invalid. |
404 | Ledger does not exist; or the bearer cannot can_read. |
5xx | Storage / nameservice errors during walk. |
Reference implementation
| Concern | Canonical location |
|---|---|
| HTTP route + auth | fluree-db-server/src/routes/log.rs::log_ledger_tail |
| Underlying API | fluree_db_api::Fluree::commit_log |
| Walk + summary | fluree_db_core::commit::walk_commit_summaries |
RDF Export Contract
fluree export --remote issues:
POST {api_base_url}/export/{ledger}
Content-Type: application/json
{
"format": "turtle",
"all_graphs": false,
"graph": "http://example.org/people",
"context": { "ex": "http://example.org/" },
"at": "t:42"
}
| Field | Type | Required | Server default | Description |
|---|---|---|---|---|
format | string | No | "turtle" | One of: turtle/ttl, ntriples/nt, nquads/n-quads, trig, jsonld/json-ld/json. Case-insensitive. |
all_graphs | bool | No | false | Export every named graph as a dataset. Requires format ∈ trig / nquads. Mutually exclusive with graph. |
graph | string | No | — | IRI of a single named graph to export. Mutually exclusive with all_graphs. |
context | object | No | ledger default | Prefix map for Turtle/TriG/JSON-LD output. Either a bare object ({ "ex": "..." }) or { "@context": {...} }. Falls back to the ledger's stored default context when absent. |
at | string | No | latest | Time spec — integer ("42"), ISO-8601 datetime ("2026-01-15T10:30:00Z"), or commit CID prefix ("bafy…"). Identical to the local --at flag. |
An empty body is accepted and treated as all-default (Turtle export at HEAD).
Auth
Admin-protected. Same middleware as /create, /drop, /reindex,
and the branch admin endpoints — registered through
v1_admin_protected_routes in fluree-db-server/src/routes/mod.rs.
Export today does not apply per-flake policy filtering: it reads
straight from the binary index. Putting it in the data-read bracket
alongside /query and /show would be a bulk policy bypass for any
bearer with can_read(ledger). Adding policy-filtered streaming export
would let it move to read-auth in the future.
Response (200 OK)
The body is the raw RDF for the requested format. Content-Type reflects
the chosen format:
| Format | Content-Type |
|---|---|
| Turtle | text/turtle; charset=utf-8 |
| N-Triples | application/n-triples; charset=utf-8 |
| N-Quads | application/n-quads; charset=utf-8 |
| TriG | application/trig; charset=utf-8 |
| JSON-LD | application/ld+json; charset=utf-8 |
The reference server today buffers the full export in memory before responding (simple, sufficient for moderate-size ledgers). Implementations are free to stream chunked bodies; clients MUST be prepared to read until EOF.
Required semantics
- Format validation. Reject unknown format strings with
400. - Dataset/format coupling. When
all_graphs == true,formatmust betrigornquads; otherwise return400with a message that mentions the dataset format requirement (the local CLI surfaces the same error). - Time spec parsing. Same rules as the merge-preview / show
contracts: parse as integer first (
t), then as ISO-8601 if it contains both-and:, else as a commit CID prefix. - Graph IRI resolution. When
graphis set, resolve via the ledger's graph registry; an unknown IRI is a400(or5xxif you treat it as a config error — the reference returns400viaApiError::Config). - Index requirement. Export reads from the binary index. If the
ledger has no index, the reference server surfaces
ApiError::Config("no binary index available for export (is the ledger indexed?)"), which the error mapper returns as400 Bad Request. Document that shape if you implement equivalently — the CLI surfaces the message verbatim.
Error responses
| Status | When |
|---|---|
400 | Unknown format; conflicting all_graphs + graph; all_graphs with non-dataset format; unknown graph IRI; malformed JSON; ledger not indexed. |
401 / 403 | Admin token required and absent/invalid. |
404 | Ledger does not exist. |
5xx | Storage / nameservice / encoding errors during walk. |
Reference implementation
| Concern | Canonical location |
|---|---|
| HTTP route + auth | fluree-db-server/src/routes/export.rs::export_ledger_tail |
| Builder | fluree_db_api::export_builder::ExportBuilder |
| Format encoders | fluree_db_api::export |
/create Contract
- Endpoint:
POST {api_base_url}/create - Request body:
{"ledger": "mydb:main"} - Response (201 Created):
{"ledger": "mydb:main", "t": 0} - Response (409 Conflict): ledger already exists
If no branch suffix is provided (e.g., "mydb"), the server MUST normalize to "mydb:main".
Used by fluree publish (which calls /create after /exists returns false) and by fluree create --remote <name> (empty-ledger creation on a remote server).
/reindex Contract
- Endpoint:
POST {api_base_url}/reindex - Auth: admin-protected (same middleware as
/create,/drop). - Request body:
{ "ledger": "mydb:main", "opts": { } }optsis optional and reserved for future per-request overrides (e.g. indexer tuning). Servers MUST accept it and MAY ignore it — today the reference server always reindexes using its own configured indexer settings. - Response (200 OK):
{ "ledger_id": "mydb:main", "index_t": 42, "root_id": "fluree:index:sha256:...", "stats": { "flake_count": 0, "leaf_count": 0, "branch_count": 0, "total_bytes": 0 } } - Response (4xx/5xx): standard
ApiErrorenvelope on failure (e.g. ledger not found).
The response shape mirrors fluree_db_api::ReindexResult — implementers should treat that Rust struct as the source of truth and add new fields only additively. Used by fluree reindex --remote <name> and by the CLI's auto-routing when a local server is running.
/exists Response Contract
- Endpoint:
GET {api_base_url}/exists?ledger=mydb:main(or viafluree-ledgerheader) - Response (200 OK, always):
{"ledger": "mydb:main", "exists": true|false}
MUST return 200 regardless of whether the ledger exists (the exists field carries the result). Should query the nameservice only — no ledger data loading.
/info Response Contract (CLI Minimum)
The CLI currently treats GET {api_base_url}/info/*ledger as an opaque JSON object, but it requires these fields:
t(integer): required forfluree cloneandfluree pullpreflight and forfluree pushconflict checks.commitId(string CID): required forfluree pushwhent > 0so it can detect divergence.ledger.named-graphs(array): required forfluree graph list. See the Graph List Contract for the per-entry schema (iri,g-id,flakes,size). A top-levelnamed-graphsarray is also accepted for older response shapes; new implementations should nest it underledger.
Other fields are optional and may be used only for display.
Origin-Based Replication (LedgerConfig)
The CLI can do origin-based clone --origin and pull fallback without a named remote by fetching objects via:
GET {api_base_url}/storage/objects/:cid?ledger=:ledger-id
If your nameservice advertises config_id on the NsRecord, the CLI will attempt to fetch that LedgerConfig blob (by CID) and then use it to try additional origins.
Graph Source Endpoints (Iceberg, R2RML, BM25, etc.)
The CLI routes graph source operations through the server when one is running. This uses the same auto-routing mechanism as query/insert/etc.: the CLI checks for server.meta.json (written by fluree server start), verifies the PID is alive, and routes through http://{listen_addr}/v1/fluree. Users can bypass with --direct.
fluree list (includes graph sources)
GET {api_base_url}/ledgers
Returns a JSON array of both ledger records and graph source records. Retracted records are excluded.
Response fields (required for each entry):
| Field | Type | Description |
|---|---|---|
name | string | Ledger or graph source name |
branch | string | Branch name (e.g., "main") |
type | string | One of: "Ledger", "Iceberg", "R2RML", "BM25", "Vector", "Geo" |
t | integer | commit_t for ledgers, index_t for graph sources (0 if not indexed) |
Example response:
[
{ "name": "mydb", "branch": "main", "type": "Ledger", "t": 5 },
{ "name": "warehouse-orders", "branch": "main", "type": "Iceberg", "t": 0 },
{ "name": "my-search", "branch": "main", "type": "BM25", "t": 5 }
]
The CLI shows a TYPE column only when the response contains non-Ledger entries.
Error responses: 500 on internal failure. Empty array [] when no records exist.
fluree info <name> (graph source fallback)
GET {api_base_url}/info/*name
Existing endpoint, extended with graph source fallback. Resolution order:
- Look up
nameas a ledger — if found, return the standard ledger info response (unchanged) - Look up
nameas a graph source (append:mainif no branch suffix) — if found, return the graph source response below - Return
404 Not Found
Graph source response fields:
| Field | Type | Description |
|---|---|---|
name | string | Graph source name |
branch | string | Branch name |
type | string | Source type (e.g., "Iceberg") |
graph_source_id | string | Canonical ID (e.g., "warehouse-orders:main") |
retracted | boolean | Whether retracted |
index_t | integer | Index watermark |
index_id | string? | Index ContentId (omitted if none) |
dependencies | string[]? | Source ledger IDs (omitted if empty) |
config | object? | Parsed configuration JSON (omitted if empty/{}) |
Example:
{
"name": "warehouse-orders",
"branch": "main",
"type": "Iceberg",
"graph_source_id": "warehouse-orders:main",
"retracted": false,
"index_t": 0,
"config": {
"catalog": {
"type": "rest",
"uri": "https://polaris.example.com/api/catalog",
"warehouse": "my-warehouse"
},
"table": "sales.orders",
"io": {
"vended_credentials": true,
"s3_region": "us-east-1"
}
}
}
CLI detection: The CLI distinguishes graph source responses from ledger responses by checking for the graph_source_id field in the JSON.
fluree drop <name> (graph source fallback)
POST {api_base_url}/drop
Existing endpoint, extended with graph source fallback. Request body is unchanged: { "ledger": "<name>", "hard": true }.
Resolution order:
- Try dropping
nameas a ledger — if the drop report hasstatus: "dropped"orstatus: "already_retracted", return that - If the ledger drop report has
status: "not_found", try dropping as a graph source (default branch"main") - If both return not found, return the not-found response
Response: Same schema as ledger drop: { "ledger_id": "name:branch", "status": "dropped"|"already_retracted"|"not_found", "files_deleted": 23, "warnings": [...] }. files_deleted is omitted when zero and is currently omitted for graph-source fallback responses. For graph sources, ledger_id contains the graph source ID (e.g., "warehouse-orders:main").
fluree iceberg map (Iceberg graph source creation)
POST {api_base_url}/iceberg/map(admin-protected)
Creates an Iceberg graph source with an R2RML mapping that defines how table rows become RDF triples. This is a write operation and should be admin-protected (same middleware as /create and /drop).
Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Graph source name (no colons) |
mode | string | No | "rest" (default) or "direct" |
catalog_uri | string | REST mode | REST catalog URI |
table | string | No | Table identifier (namespace.table); required for REST mode if not specified in R2RML mapping |
table_location | string | Direct mode | S3 URI (s3://bucket/path/to/table) |
r2rml | string | Yes | R2RML mapping source (storage address or path) |
r2rml_type | string | No | Mapping media type (e.g., "text/turtle"); inferred from extension |
branch | string | No | Branch name (default: "main") |
auth_bearer | string | No | Bearer token for REST catalog auth |
oauth2_token_url | string | No | OAuth2 token endpoint |
oauth2_client_id | string | No | OAuth2 client ID |
oauth2_client_secret | string | No | OAuth2 client secret |
warehouse | string | No | Warehouse identifier (REST mode) |
no_vended_credentials | boolean | No | Disable vended credentials (default: false) |
s3_region | string | No | S3 region override |
s3_endpoint | string | No | S3 endpoint override (MinIO, LocalStack) |
s3_path_style | boolean | No | Use path-style S3 URLs (default: false) |
Validation rules:
namemust not be empty or contain:r2rmlis required (defines how table rows become RDF triples)- REST mode requires
catalog_uri; requirestableunless specified in R2RML mapping'srr:tableName - Direct mode requires
table_location(must start withs3://ors3a://) - OAuth2 fields must all be provided together (url + id + secret)
Example — REST catalog with R2RML:
{
"name": "warehouse-orders",
"mode": "rest",
"catalog_uri": "https://polaris.example.com/api/catalog",
"table": "sales.orders",
"r2rml": "mappings/orders.ttl",
"auth_bearer": "my-token",
"warehouse": "my-warehouse"
}
Example — REST catalog (table inferred from R2RML rr:tableName):
{
"name": "airlines",
"mode": "rest",
"catalog_uri": "https://polaris.example.com/api/catalog",
"r2rml": "mappings/airlines.ttl",
"auth_bearer": "my-token"
}
Example — Direct S3 (no catalog):
{
"name": "execution-log",
"mode": "direct",
"table_location": "s3://bucket/warehouse/logs/execution_log",
"r2rml": "mappings/execution_log.ttl",
"s3_region": "us-east-1"
}
Response (201 Created):
| Field | Type | Present | Description |
|---|---|---|---|
graph_source_id | string | Always | Created ID (e.g., "warehouse-orders:main") |
table_identifier | string | Always | Table identifier or derived from location |
catalog_uri | string | Always | Catalog URI or S3 location |
connection_tested | boolean | Always | Whether catalog connection was verified (always false for direct mode) |
mapping_source | string | Always | R2RML mapping source |
triples_map_count | integer | Always | Number of TriplesMap definitions found |
mapping_validated | boolean | Always | Whether mapping was parsed and compiled successfully |
Error responses:
400 Bad Request— validation failures (missing fields, invalid mode, bad table identifier)409 Conflict— graph source with this name already exists (if your nameservice enforces uniqueness)500 Internal Server Error— catalog connection failure, mapping load failure, nameservice write failure
Querying graph sources
Graph source queries work through normal query endpoints. No separate endpoint is needed, but the Rust API has an important distinction:
- Use
query_from()when the query body carries the dataset ("from"in JSON-LD,FROM/FROM NAMEDin SPARQL), or when you are composing multiple sources. - Use
graph(alias).query()for a single lazy query target that may be either a native ledger or a mapped graph source. - Do not use the raw materialized-snapshot path (
fluree.db(&alias)→fluree.query(&view, ...)) for graph source aliases.
Important: The unsupported path is specifically the raw
GraphDbsnapshot flow (fluree.db(&alias)→fluree.query(&view, ...)). That API assumes you already loaded a native ledger snapshot. Graph source resolution happens in the lazy builder paths (graph().query()andquery_from()), which wire in the R2RML provider and can fall back from "ledger not found" to "mapped graph source".
Supported query paths:
// Connection-level — graph sources resolve transparently
// When compiled with the `iceberg` feature, query_from() automatically
// enables R2RML provider support via .with_r2rml().
f.query_from().sparql(sparql).execute_formatted().await
f.query_from().jsonld(&query_json).execute_formatted().await
// Single-target lazy query — works for ledgers and mapped graph sources
f.graph(alias).query().sparql(sparql).execute_formatted().await
// Ledger-scoped query that may reference graph sources in GRAPH patterns
f.graph(ledger_id).query().sparql(sparql).execute_formatted().await
Do NOT use:
// Raw materialized snapshot path — native ledgers only
let view = f.db(&alias).await?;
f.query(&view, query_input).await? // ❌ No R2RML, no graph source resolution
Query patterns that reference graph sources:
Graph sources can be queried directly, just like ledgers:
POST {api_base_url}/query/execution-log:mainwith a SPARQL or JSON-LD query body
Via FROM / FROM NAMED clauses:
SELECT * FROM <execution-log:main> WHERE { ?s ?p ?o } LIMIT 10
Via GRAPH patterns (joining with ledger data):
SELECT ?name ?orderId ?total
FROM <mydb:main>
WHERE {
?customer schema:name ?name .
?customer ex:customerId ?custId .
GRAPH <warehouse-orders:main> {
?order ex:customerId ?custId .
?order ex:orderId ?orderId .
?order ex:total ?total .
}
}
How it works: When the iceberg feature is compiled, query_from() and graph().query() automatically call .with_r2rml(), which constructs a FlureeR2rmlProvider that can resolve graph source names to R2RML mappings and route triple patterns through the Iceberg scan engine. The NameService trait requires GraphSourceLookup (read-only graph source discovery), so graph source resolution is always available at the nameservice layer.
Known limitation: FROM <ledger>, <graph-source> with bare WHERE patterns (no GRAPH wrapper) — the graph source participates in the dataset but bare triple patterns only scan native indexes. Use explicit GRAPH <gs:main> { ... } for the graph source part in mixed-source queries.
Authentication
POST /iceberg/mapandPOST /dropare admin-protected (same middleware as/create)GET /ledgersandGET /info/*nameare read-only (same auth as other read endpoints)POST /query/*ledgerwith graph source GRAPH patterns uses normal query auth
Ledger Portability (.flpack Files)
The CLI supports exporting and importing full native ledgers as .flpack files using the fluree-pack-v1 wire format. This enables ledger portability without a running server.
# Export a ledger (all commits + indexes + dictionaries)
fluree export mydb --format ledger -o mydb.flpack
# Import into a new instance (can use a different ledger name)
fluree create imported-db --from mydb.flpack
The .flpack format is identical to the binary stream served by POST /pack/{ledger}, with the addition of a nameservice manifest frame that carries the metadata needed to reconstruct the nameservice record on import:
{
"phase": "nameservice",
"ledger_id": "original-name:main",
"name": "original-name",
"branch": "main",
"commit_head_id": "bafybeig...commitHead",
"commit_t": 42,
"index_head_id": "bafybeig...indexRoot",
"index_t": 40
}
Aliasing on import: The ledger name provided to fluree create determines the local storage path. The data itself is content-addressed (CIDs), so a ledger can be imported under any name. The ledger_id inside the index root binary is informational and does not affect CAS resolution.
Combined with publish: A typical workflow for moving a ledger from one environment to another:
# On source machine: export
fluree export mydb --format ledger -o mydb.flpack
# On target machine: import and publish to server
fluree create mydb --from mydb.flpack
fluree remote add prod https://prod.example.com
fluree auth login --remote prod
fluree publish prod mydb
Quick Validation Script
From a clean project directory:
fluree init
fluree remote add origin http://localhost:8090
fluree auth login --remote origin --token @token.txt
# Ledger operations
fluree fetch origin
fluree clone origin mydb:main
fluree pull mydb:main
fluree push mydb:main
# Publish a local ledger to remote
fluree create local-db
fluree insert local-db -e '{"@id": "ex:test", "ex:val": 1}'
fluree publish origin local-db
# Export / import round-trip
fluree export mydb --format ledger -o mydb.flpack
fluree create imported --from mydb.flpack
# Iceberg operations (requires iceberg feature on server)
fluree iceberg map my-gs \
--catalog-uri https://polaris.example.com/api/catalog \
--r2rml mappings/orders.ttl \
--auth-bearer $POLARIS_TOKEN
fluree list # should show mydb (Ledger) + my-gs (Iceberg)
fluree info my-gs # should show Iceberg config + R2RML mapping
fluree show t:1 --remote origin # should show decoded commit with resolved IRIs
fluree log mydb --remote origin --oneline # should print the remote's commit chain newest-first
fluree export mydb --remote origin --format turtle > mydb-remote.ttl # should write Turtle to disk
fluree context get mydb --remote origin # should print the remote ledger's default context
fluree context set mydb --remote origin -e '{"ex": "http://example.org/"}' # admin: replace context
fluree history http://example.org/alice --ledger mydb --remote origin --format json # remote history
fluree query mydb 'SELECT * WHERE { ?s ?p ?o }' --remote origin --at 1 # time-travel via /query/{ledger}
fluree query mydb 'SELECT * WHERE { ?s ?p ?o }' --remote origin --at 1 --explain --format json # time-travel explain via /explain/{ledger}
fluree create empty-db --remote origin # should create an empty ledger on the remote
fluree export mydb --remote origin --format ledger -o mydb-remote.flpack # archive remote ledger
fluree drop my-gs --force # should drop the graph source locally
fluree drop local-db --remote origin --force # should drop the published ledger on the remote