NabuStore
API reference

API Reference

Complete REST API endpoint documentation


Description

The Nabu Store REST API exposes three services — BlobService, ClusterService, and InternalService — over HTTP/JSON (transcoded from gRPC). Use BlobService to store, retrieve, list, and delete binary objects (blobs) in your AI inference pipeline. Use ClusterService to manage cluster membership, monitor topology, and synchronize the consistent-hash ring. InternalService handles node-to-node replication and shard operations and is not intended for direct client use.


Parameters

BlobService

PUT /v1/blobs — Store a blob

ParameterTypeRequiredDescription
databytes (base64 in JSON)YesRaw binary content of the blob to store.
policystring (enum)NoReplication policy to apply. One of REPLICATION_POLICY_NONE, REPLICATION_POLICY_REPLICA2, REPLICATION_POLICY_REPLICA3, REPLICATION_POLICY_EC42, REPLICATION_POLICY_EC82. Defaults to REPLICATION_POLICY_REPLICA3 when unset or REPLICATION_POLICY_UNSPECIFIED.
labelsmap<string, string>NoArbitrary key-value metadata attached to the blob at write time.

POST /v1/blobs/stream — Store a blob via client-side streaming (PutStream)

Each chunk in the stream is a PutChunk message:

FieldTypeRequiredDescription
idBlobIDNoBlob identifier. Omit on the first chunk; the server assigns it.
databytesYesChunk payload.
finalboolYesSet true on the last chunk to signal end-of-stream.
policystring (enum)NoReplication policy, same values as PUT /v1/blobs. Include on the first chunk.

GET /v1/blobs/{id} — Retrieve a blob

ParameterTypeRequiredDescription
idbytes (hex or base64 in path)Yes16-byte blob identifier returned by a previous Put.

GET /v1/blobs/{id}/stream — Retrieve a blob via server-side streaming (GetStream)

ParameterTypeRequiredDescription
idbytesYes16-byte blob identifier.

DELETE /v1/blobs/{id} — Delete a blob

ParameterTypeRequiredDescription
idbytesYes16-byte blob identifier.

GET /v1/blobs/{id}/stat — Retrieve blob metadata

ParameterTypeRequiredDescription
idbytesYes16-byte blob identifier.

GET /v1/blobs — List blobs

ParameterTypeRequiredDescription
cursorbytes (base64)NoOpaque pagination cursor returned in a previous ListResponse.next_cursor. Omit to start from the beginning.
limitint32NoMaximum number of blob IDs to return per page. Defaults to 100 when omitted or <= 0.
prefixstringNoReturn only blob IDs whose string representation begins with this prefix.

ClusterService

POST /v1/cluster/join — Add a node to the cluster

ParameterTypeRequiredDescription
node_idstringYesUnique identifier for the joining node.
addressstringYesReachable network address (host:port) of the joining node.
capacity_bytesuint64YesTotal storage capacity in bytes that this node contributes.

POST /v1/cluster/leave — Remove a node from the cluster

ParameterTypeRequiredDescription
node_idstringYesIdentifier of the node that is leaving.
gracefulboolNoIf true, the node is marked as leaving and data migration is initiated before removal. If false or omitted, the node is removed immediately.

POST /v1/cluster/heartbeat — Send a node heartbeat

ParameterTypeRequiredDescription
node_idstringYesIdentifier of the reporting node.
used_bytesuint64YesCurrent storage usage in bytes on the reporting node.
blob_countuint64YesNumber of blobs stored on the reporting node.
ring_versionint64YesThe consistent-hash ring version the node currently holds. Used to detect stale topology.

GET /v1/cluster/state — Get cluster topology

No request parameters.

POST /v1/cluster/sync-ring — Synchronize the hash ring

ParameterTypeRequiredDescription
ring_versionint64YesThe ring version the caller wants to apply. The server updates its ring only if this version is newer than its own.

Returns

BlobService

PUT /v1/blobsPutResponse

Returns on successful write:

{
  "id": { "id": "<base64-encoded 16-byte blob ID>" },
  "size": 204800
}
FieldTypeDescription
id.idbytesAssigned 16-byte blob identifier, derived from a content hash.
sizeint64Number of bytes stored.

POST /v1/blobs/streamPutResponse

Same shape as PutResponse above, returned after the server receives a chunk with final: true.

GET /v1/blobs/{id}GetResponse

{
  "data": "<base64-encoded blob bytes>",
  "meta": {
    "size": 204800,
    "created_at": 1718000000000000000,
    "modified_at": 1718000000000000000,
    "checksum": "<base64-encoded checksum>",
    "labels": { "model": "llama3", "tenant": "acme" }
  }
}

If the blob is not found locally, the server transparently attempts retrieval from other cluster nodes before returning an error.

GET /v1/blobs/{id}/stream — server-streaming GetChunk messages

Each message contains:

FieldTypeDescription
databytesChunk payload (up to 1 MB per chunk).
finalbooltrue on the last chunk.

DELETE /v1/blobs/{id}DeleteResponse

{ "deleted": true }

deleted is true whether or not the blob existed locally; the server removes it from the index regardless.

GET /v1/blobs/{id}/statStatResponse

{
  "exists": true,
  "meta": {
    "size": 204800,
    "created_at": 1718000000000000000,
    "modified_at": 1718000000000000000,
    "checksum": "<base64-encoded checksum>",
    "labels": {}
  }
}

If the blob does not exist, exists is false and meta is omitted.

GET /v1/blobsListResponse

{
  "ids": [
    { "id": "<base64-encoded blob ID>" }
  ],
  "next_cursor": "<base64-encoded opaque cursor>",
  "has_more": true
}

next_cursor is present only when has_more is true. Pass it as cursor in your next request to retrieve the following page.


ClusterService

POST /v1/cluster/joinJoinResponse

{
  "accepted": true,
  "ring_version": 4,
  "nodes": [
    {
      "node_id": "node-1",
      "address": "10.0.0.1:7000",
      "state": "active",
      "capacity_bytes": 107374182400,
      "used_bytes": 5368709120,
      "blob_count": 1024
    }
  ]
}
FieldTypeDescription
acceptedbooltrue if the node was admitted (including if it was already a member).
ring_versionint64Current version of the consistent-hash ring after the join.
nodesNodeInfo[]Full list of cluster members at the time of the join.

POST /v1/cluster/leaveLeaveResponse

{ "acknowledged": true }

POST /v1/cluster/heartbeatHeartbeatResponse

{
  "ok": true,
  "ring_version": 4,
  "nodes": []
}

nodes is populated only when the caller's ring_version is behind the server's current version, giving the caller everything it needs to converge in a single round-trip. If the node is not recognized, ok is false and the server instructs the caller to rejoin.

GET /v1/cluster/stateGetClusterStateResponse

{
  "nodes": [
    {
      "node_id": "node-1",
      "address": "10.0.0.1:7000",
      "state": "active",
      "capacity_bytes": 107374182400,
      "used_bytes": 5368709120,
      "blob_count": 1024
    }
  ],
  "ring_version": 4
}

POST /v1/cluster/sync-ringSyncRingResponse

{
  "ring_version": 5,
  "node_ids": ["node-1", "node-2", "node-3"],
  "vnodes_per_node": 150
}

Errors

All errors are returned as standard HTTP status codes with a JSON error body:

{
  "code": <grpc-status-code>,
  "message": "<human-readable description>"
}

BlobService errors

HTTP StatusgRPC CodeCondition
400 Bad RequestINVALID_ARGUMENTid field is missing, empty, or not exactly 16 bytes. Applies to Get, Delete, Stat, and both stream variants.
404 Not FoundNOT_FOUNDBlob does not exist on the local node and could not be retrieved from any other cluster node (Get, GetStream).
500 Internal Server ErrorINTERNALBackend storage failure during Put, Get, or Delete.
501 Not ImplementedUNIMPLEMENTEDThe called method has no registered handler (returned by UnimplementedBlobServiceServer).
500 Internal Server ErrorINTERNALPutStream received no header chunk before EOF.

ClusterService errors

HTTP StatusgRPC CodeCondition
404 Not FoundNOT_FOUNDHeartbeat received from a node_id that is not in the cluster. The response body includes commands: ["rejoin"].
500 Internal Server ErrorINTERNALUnexpected failure during ring manipulation or state persistence.
501 Not ImplementedUNIMPLEMENTEDThe called method has no registered handler.

General errors

HTTP StatusgRPC CodeCondition
401 UnauthorizedUNAUTHENTICATEDRequest is missing a valid bearer token.
403 ForbiddenPERMISSION_DENIEDToken is valid but the caller lacks permission for this operation.
503 Service UnavailableUNAVAILABLEThe target node is temporarily unreachable or restarting. Retry with exponential back-off.

Examples

Store a blob

Store a small binary payload with a REPLICA3 policy and a custom label.

curl -X PUT https://nabu.example.com/v1/blobs \
  -H "Authorization: Bearer $NABU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "data": "SGVsbG8gTmFidSBTdG9yZSE=",
    "policy": "REPLICATION_POLICY_REPLICA3",
    "labels": {
      "model": "llama3",
      "tenant": "acme"
    }
  }'

Expected response (200 OK)

{
  "id": { "id": "AQIDBAUGBwgJCgsMDQ4P" },
  "size": 16
}

Save the id.id value — you need it for all subsequent operations on this blob.


Retrieve a blob

Fetch the full blob content and its metadata by ID.

curl -X GET https://nabu.example.com/v1/blobs/AQIDBAUGB wgJCgsMDQ4P \
  -H "Authorization: Bearer $NABU_TOKEN"

Expected response (200 OK)

{
  "data": "SGVsbG8gTmFidSBTdG9yZSE=",
  "meta": {
    "size": 16,
    "created_at": 1718000000000000000,
    "modified_at": 1718000000000000000,
    "checksum": "c2hhMjU2aGFzaA==",
    "labels": {
      "model": "llama3",
      "tenant": "acme"
    }
  }
}

Inspect blob metadata without downloading content

Use stat when you need to check existence or read labels without transferring the payload.

curl -X GET https://nabu.example.com/v1/blobs/AQIDBAUGB wgJCgsMDQ4P/stat \
  -H "Authorization: Bearer $NABU_TOKEN"

Expected response — blob exists (200 OK)

{
  "exists": true,
  "meta": {
    "size": 16,
    "created_at": 1718000000000000000,
    "modified_at": 1718000000000000000,
    "checksum": "c2hhMjU2aGFzaA==",
    "labels": { "model": "llama3" }
  }
}

Expected response — blob not found (200 OK)

{ "exists": false }

List blobs with pagination

Retrieve the first page of up to 50 blob IDs.

curl -X GET "https://nabu.example.com/v1/blobs?limit=50" \
  -H "Authorization: Bearer $NABU_TOKEN"

Expected response (200 OK)

{
  "ids": [
    { "id": "AQIDBAUGB wgJCgsMDQ4P" },
    { "id": "EBESExQVFhcYGRobHB0e" }
  ],
  "next_cursor": "HyAhIiMkJSYn",
  "has_more": true
}

Fetch the next page by passing next_cursor as cursor:

curl -X GET "https://nabu.example.com/v1/blobs?limit=50&cursor=HyAhIiMkJSYn" \
  -H "Authorization: Bearer $NABU_TOKEN"

Delete a blob

curl -X DELETE https://nabu.example.com/v1/blobs/AQIDBAUGB wgJCgsMDQ4P \
  -H "Authorization: Bearer $NABU_TOKEN"

Expected response (200 OK)

{ "deleted": true }

Join a new node to the cluster

Call this from the new node (or your orchestration layer) after the node process is running.

curl -X POST https://nabu.example.com/v1/cluster/join \
  -H "Authorization: Bearer $NABU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "node_id": "node-4",
    "address": "10.0.0.4:7000",
    "capacity_bytes": 107374182400
  }'

Expected response (200 OK)

{
  "accepted": true,
  "ring_version": 5,
  "nodes": [
    { "node_id": "node-1", "address": "10.0.0.1:7000", "state": "active", "capacity_bytes": 107374182400, "used_bytes": 5368709120, "blob_count": 1024 },
    { "node_id": "node-4", "address": "10.0.0.4:7000", "state": "active", "capacity_bytes": 107374182400, "used_bytes": 0, "blob_count": 0 }
  ]
}

Get cluster state

Poll this endpoint from your monitoring system or health dashboard.

curl -X GET https://nabu.example.com/v1/cluster/state \
  -H "Authorization: Bearer $NABU_TOKEN"

Expected response (200 OK)

{
  "nodes": [
    { "node_id": "node-1", "address": "10.0.0.1:7000", "state": "active", "capacity_bytes": 107374182400, "used_bytes": 5368709120, "blob_count": 1024 },
    { "node_id": "node-2", "address": "10.0.0.2:7000", "state": "active", "capacity_bytes": 107374182400, "used_bytes": 4831838208, "blob_count": 987 },
    { "node_id": "node-3", "address": "10.0.0.3:7000", "state": "offline", "capacity_bytes": 107374182400, "used_bytes": 0, "blob_count": 0 }
  ],
  "ring_version": 5
}

Graceful node removal

Mark a node as leaving so the cluster migrates its data before removing it from the ring.

curl -X POST https://nabu.example.com/v1/cluster/leave \
  -H "Authorization: Bearer $NABU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "node_id": "node-3", "graceful": true }'

Expected response (200 OK)

{ "acknowledged": true }

Poll GET /v1/cluster/state until node-3 no longer appears or its state transitions to leaving and then disappears from the list.


Notes

Blob IDs

Blob IDs are 16-byte content-addressed identifiers derived from a hash of the uploaded data. Storing the same bytes twice returns the same ID — the second write is a no-op if the blob already exists on the backend. There is no separate "update" operation; to change a blob's content you must store new bytes (which produces a new ID) and delete the old one.

Replication policies

Policy enum valueMeaning
REPLICATION_POLICY_UNSPECIFIEDTreated identically to REPLICATION_POLICY_REPLICA3 by the server. Avoid relying on this behavior — always specify an explicit policy.
REPLICATION_POLICY_NONENo redundancy. Use only for ephemeral or easily re-generated data.
REPLICATION_POLICY_REPLICA2Two full copies across two different nodes.
REPLICATION_POLICY_REPLICA3Three full copies. Default for most workloads.
REPLICATION_POLICY_EC42Erasure coding, 4 data shards + 2 parity shards. Requires at least 6 nodes.
REPLICATION_POLICY_EC82Erasure coding, 8 data shards + 2 parity shards. Requires at least 10 nodes; highest storage efficiency.

The policy is set at write time and is stored in blob metadata. You cannot change a blob's replication policy after it has been stored — delete and re-upload with the new policy instead.

Streaming endpoints

  • PutStream is a client-streaming RPC. Send chunks in order, set final: true on the last chunk, then close the send stream. The server responds once with a single PutResponse. Do not interleave chunks from different blobs in a single stream.
  • GetStream is a server-streaming RPC. The server sends chunks of up to 1 MB each. The last chunk has final: true. Buffer or pipe the chunks in arrival order.
  • Use streaming variants for blobs larger than a few MB to avoid holding large payloads in memory.

Pagination

The cursor in ListRequest and next_cursor in ListResponse are opaque server-generated values. Do not attempt to parse or construct them manually. A cursor is valid only for the node that issued it.

Heartbeat and cluster convergence

Each node must send a Heartbeat to the leader (or any peer, depending on your deployment topology) at regular intervals. If a node's ring_version in the heartbeat is behind the server's current version, the heartbeat response includes the full nodes list so the caller can catch up within a single round-trip — no separate SyncRing call is needed during normal operation.

Replication is best-effort on Put

A successful PutResponse confirms the blob is durably stored on the receiving node and that replication to peer nodes was attempted. Replication failures are logged as warnings but do not cause the Put to fail. Poll GET /v1/cluster/state and your monitoring stack to verify replica health independently.

Delete propagation

As of the current release, DELETE /v1/blobs/{id} removes the blob from the local node and its index only. Propagation to replica nodes is not yet implemented. To ensure full removal across the cluster, issue the DELETE request to each node or implement a reconciliation loop in your application layer.

InternalService is node-to-node only

The InternalService endpoints (Replicate, ReplicateStream, DeleteInternal, FetchShard, StoreShard, Migrate) are used exclusively for intra-cluster communication. Do not call these endpoints from your application code. They are not authenticated with user-facing credentials and their request/response shapes may change without notice.

Timestamps

created_at and modified_at in BlobMeta are Unix nanosecond timestamps (int64). Divide by 1e9 to convert to seconds.