NuclaDB

API Reference

Exact REST/JSON and gRPC request/response shapes, extracted from gateway.go and nucladb.proto.

REST / JSON

Default port :8080.

MethodPathMaps to
POST/v1/tenantsCreateTenant
GET/v1/tenantsListTenants
DELETE/v1/tenants/{id}DeleteTenant
PUT/v1/tenants/{id}/quotaSetQuota
POST/v1/vectorsInsert
POST/v1/vectors:batchBatchUpsert
GET/v1/vectors/{id}Get
PATCH/v1/vectors/{id}UpdateMetadata
DELETE/v1/vectors/{id}Delete
GET/v1/vectorsList (page_size, page_token)
GET/v1/vectors:countCount
POST/v1/searchSearch
GET/docsCLI documentation page (embedded static HTML)

tenant_id is a JSON body field on requests with a body, or a ?tenant_id= query parameter on the rest. Omitting it uses the reserved default tenant (no quota). Request bodies over -max-message-bytes (64 MiB) get a 413.

The responses below were recorded against a real local nucladbd.

Tenants

// POST /v1/tenants
// dim and metric are optional and default to the server's -dim and -metric
{ "tenant_id": "acme", "dim": 2, "metric": "cosine", "max_vectors": 1000 }
// response
{}

// PUT /v1/tenants/acme/quota
{ "max_vectors": 5000, "max_qps": 50 }
// response
{}

// GET /v1/tenants  (metric is the DistanceMetric enum number)
{"tenants":[{"tenant_id":"acme","quota":{"max_vectors":5000,"max_qps":50},"dim":2,"metric":1},
            {"tenant_id":"default","vector_count":2,"quota":{},"dim":4,"metric":2}]}

// DELETE /v1/tenants/acme  (removes its files; "default" can't be deleted)
{}

Creating a tenant that already exists returns 409.

Write vectors

// POST /v1/vectors
{ "id": "1", "values": [1, 0, 0, 0], "metadata": {"team": "search"} }
// response
{ "id": "1" }

// POST /v1/vectors:batch  (one shared WAL fsync per tenant in the batch)
{ "vectors": [
  { "id": "1", "values": [1, 0, 0, 0], "metadata": {"team": "search", "year": "2023"} },
  { "id": "2", "values": [0, 1, 0, 0], "metadata": {"team": "ads", "year": "2025"} }
] }
// response
{"upserted":2}

// PATCH /v1/vectors/2  (replaces all metadata, keeps the vector)
{ "metadata": {"team": "infra", "year": "2025"} }
// response
{}

// DELETE /v1/vectors/{id}?tenant_id=acme
{ "deleted": true }

Vector ids are decimal uint64 strings.

Read vectors

// GET /v1/vectors/2
{"vector":{"id":"2","values":[0,1,0,0],"metadata":{"team":"ads","year":"2025"}}}

// GET /v1/vectors?page_size=1  (pass next_page_token back as page_token)
{"ids":["1"],"next_page_token":"2"}

// GET /v1/vectors:count
{"count":2}
// POST /v1/search
{
  "query": [1, 0, 0, 0],
  "top_k": 5,              // capped at 1000
  "ef_search": 0,          // 0 = server default (64), capped at 10000
  "filters": {"team": "infra"},          // key=value matches
  "where": [{"key": "year", "op": "gte", "value": "2024"}]
}
// response
{"matches":[{"id":"2","score":2,"metadata":{"team":"infra","year":"2025"}}]}

where clauses all have to match. Ops: eq, ne, in, not_in (these two take values), gt, gte, lt, lte (compared as numbers; a non-number never matches) and exists. A filter that matches at most -exact-filter-limit vectors (4096 by default) is answered by scoring every match exactly, so a selective filter still returns a full top-k.

Errors

{"error": "<message>"}, with the HTTP status mapped from the gRPC status code: InvalidArgument → 400, Unauthenticated → 401, PermissionDenied → 403, NotFound → 404, AlreadyExists → 409, ResourceExhausted → 429, else 500.

Auth and TLS

Start nucladbd with -api-keys=keys.json to require a key on every request:

[{"key": "at-least-16-characters", "tenants": ["acme"]},
 {"key": "an-admin-key-for-ops", "tenants": ["*"], "admin": true}]

A key only reaches the tenants it lists (* for all). Creating, listing, deleting and re-quota'ing tenants needs an admin key. Send it as Authorization: Bearer <key> or X-API-Key: <key>; over gRPC, as authorization metadata. Keys are compared in constant time and must be at least 16 characters. Without -api-keys, every request is allowed.

-tls-cert and -tls-key serve both gRPC and REST over TLS. The Go CLI reads NUCLADB_API_KEY and NUCLADB_TLS=1; the Python client takes api_key= and tls=True.

gRPC

proto/nucladb.proto, default port :9090. Service nucladb.v1.NuclaDB:

rpc CreateTenant(CreateTenantRequest) returns (CreateTenantResponse);
rpc Insert(InsertRequest) returns (InsertResponse);
rpc BatchUpsert(BatchUpsertRequest) returns (BatchUpsertResponse);
rpc Delete(DeleteRequest) returns (DeleteResponse);
rpc Search(SearchRequest) returns (SearchResponse);
rpc Get(GetRequest) returns (GetResponse);
rpc List(ListRequest) returns (ListResponse);
rpc Count(CountRequest) returns (CountResponse);
rpc UpdateMetadata(UpdateMetadataRequest) returns (UpdateMetadataResponse);
rpc DeleteTenant(DeleteTenantRequest) returns (DeleteTenantResponse);
rpc SetQuota(SetQuotaRequest) returns (SetQuotaResponse);
rpc ListTenants(ListTenantsRequest) returns (ListTenantsResponse);

Key messages:

message CreateTenantRequest {
  string tenant_id = 1;
  TenantQuota quota = 2;
  int32 dim = 3;            // 0 = server default
  DistanceMetric metric = 4; // UNSPECIFIED = server default
}

message TenantQuota {
  int64 max_vectors = 1;  // 0 means unlimited
  double max_qps = 2;     // 0 means unlimited
}

message Vector {
  string id = 1;
  repeated float values = 2;
  map<string, string> metadata = 3;
  string tenant_id = 4;
}

message SearchRequest {
  repeated float query = 1;
  int32 top_k = 2;
  DistanceMetric metric = 3; // must match the tenant's metric if set
  int32 ef_search = 4;
  repeated MetadataFilter filters = 5;
  string tenant_id = 6;
}

message MetadataFilter {
  string key = 1;
  string value = 2;           // for EQ, NE and the numeric ops
  FilterOp op = 3;
  repeated string values = 4; // for IN and NOT_IN
}

enum FilterOp {
  FILTER_OP_EQ = 0; FILTER_OP_NE = 1; FILTER_OP_IN = 2; FILTER_OP_NOT_IN = 3;
  FILTER_OP_GT = 4; FILTER_OP_GTE = 5; FILTER_OP_LT = 6; FILTER_OP_LTE = 7;
  FILTER_OP_EXISTS = 8;
}

enum DistanceMetric {
  DISTANCE_METRIC_UNSPECIFIED = 0;
  DISTANCE_METRIC_COSINE = 1;
  DISTANCE_METRIC_L2 = 2;
  DISTANCE_METRIC_DOT = 3;
}

The standard gRPC health-checking protocol (grpc.health.v1.Health/Check) is also served, without a key. This is what nucladb-cli ping and the cluster health checker both use.

Server flags

cmd/nucladbd/main.go:

FlagDefaultMeaning
-data-dir./datadirectory holding WAL + snapshot files
-grpc-addr:9090gRPC listen address
-http-addr:8080REST/JSON + /metrics listen address
-dim128vector dimension for tenants created without their own
-metriccosinemetric for tenants created without their own: cosine, l2, or dot
-m16HNSW M: bidirectional links per node above layer 0
-ef-construction200HNSW build-time candidate list size
-snapshot-interval5mhow often to snapshot to disk
-max-message-bytes67108864largest gRPC message or REST body accepted
-exact-filter-limit4096filters matching at most this many vectors are scored exactly
-tenant-idle-timeout30mclose tenants unused for this long (0 keeps them open)
-api-keysJSON file of API keys; without it every request is allowed
-tls-cert / -tls-keyserve gRPC and REST over TLS
-metrics-interval15show often to refresh per-tenant usage gauges

For working examples against this API, see the CLI reference and quickstart.

On this page