Features

What hoardDB does today. Each statement is checked against the code or a test; open its evidence to see which.

Stores and data

There are six stores: hash, b-tree, FIFO, LIFO, heap and blob. A bucket declares each as a field, and every document gets its own.

But: In ring mode a heap store has no push and a blob store has no put yet, and a store's type cannot be changed after it is declared. Indexes are not available in ring mode yet.

Evidence (3)
  • cli/ringgrammar.go:ringStoreKinds
  • cli/ringgrammar.go:ringStoreWords
  • cli/ringstores.go:ringUnwiredStoreVerb

Each store reads back in its own order: a hash by key, a b-tree in key order, a FIFO queue oldest first, a LIFO stack newest first, a heap by priority, a blob as raw bytes.

But: In ring mode a heap store reads by priority but cannot be pushed to yet, and a blob store can be counted but not put to.

Evidence (2)
  • cli/ringgrammar.go:ringStoreKinds
  • cli/ringgrammar.go:ringVerbs

Data lives with what it describes: each user, order or device keeps its own stores, one set for each document, rather than a few big shared ones.

But: How many buckets or documents one node can hold has not been measured. No figure appears on this site until it has.

Evidence (2)
  • AGENTS.md:The unit is the small bucket
  • cli/ringgrammar.go:ringStoreWords

A field has a type: string, int32, int, double, decimal, number, bool, datetime, bsontimestamp, binary, uuid, objectid, object, array, regex, null or any. A date, a uuid, a decimal, an object id, bytes and a regular expression are written in Extended JSON, such as {$date: "2026-12-01T18:30:00Z"}.

But: A field's type is recorded and shown by describe bucket, but ring mode does not check a written value against it yet. The server does refuse a field name the bucket does not declare.

Evidence (2)
  • cli/ringgrammar.go:ringScalarWords
  • cluster/wire/ringadmin.go:adminScalarWords

alter bucket adds a field to a running bucket and rewrites no document: existing documents read the new field as unset.

But: A field's type never changes. alter bucket can drop a field, which makes it unreadable at once and deletes its values in the background.

Evidence (2)
  • cluster/wire/alterb.go:NewAlterBucket
  • TestAlterAddsAFieldAndEveryWriterUsesIt

The command line

The CLI is interactive: Tab completes statements and the names of databases, buckets and fields, and help is generated from the same grammar the parser reads, so it cannot list a statement that does not run.

But: Ring mode is selected with hoarddb -ring in this build.

Evidence (4)
  • cli/ringgrammar.go:ringForms
  • cli/ringcomplete.go:Completions
  • TestOneGrammarForEveryQuotedFix
  • TestHelpAndCompletionMatchTheParser

At a terminal the CLI opens with a summary of the node you reached: its name, its address and its uptime, and, in a cluster, its state, how many members the cluster has and which one leads.

But: A single node shows single node where a cluster shows its members. The summary is drawn only on a terminal, and a piped session prints two plain lines instead.

Evidence (3)
  • cli/ringbanner.go:ringBannerFacts
  • TestBannerRunInteractiveDrawsTheBox
  • TestBannerInteractiveWithRedirectedOutputKeepsPlainLines

hoardDB is not wire-compatible with any other database and does not aim to be. HQL is inspired by JSON syntax; it is a different language from every other query language.

Evidence (1)
  • cli/parser.go:ParseCommand

HQL's literals and command forms are JSON.

But: The wire format is BSON, not JSON: this claim is about what you write, not what travels.

Evidence (1)
  • cli/documents.go:parseDocument

Writing in bulk

One request can carry up to 255 writes, to any buckets. A batch is not a transaction: each write is applied or refused on its own, and after a lost answer the whole request can be sent again without applying a write twice, for as long as the writes are still inside the dedupe window.

But: Batching is in the wire protocol and the Go ring client inside the server module. The CLI has no batch statement in ring mode yet, and a batch carries writes only.

Evidence (4)
  • transport/opcodes.go:OpRingBatch
  • ringclient/batch.go:NewBatch
  • TestBatchOf256IsRefusedBeforeSending
  • TestBatchRetriesUnderTheSameRequestID

Running it

The database is a single binary, with no sidecar and no separate indexer to run.

But: The CLI is a second binary, and it is a client rather than a component of the database.

Evidence (2)
  • Makefile:build-server
  • cmd/server/main.go:main

hoarddb-server starts with no arguments and no configuration file.

Evidence (2)
  • config/config.go:DefaultConfig
  • TestNoArgumentStartPathInTempDir

Run one node, or form a cluster: join adds a node to a cluster that holds no data, and gives it a vote while the cluster has a free voter slot.

But: Join is refused once any database exists, so form the cluster first and create data after. A node that finds no free voter slot joins without a vote, and promote gives it one after a slot is freed. Copying data between nodes is not usable end to end yet, so do not count on a second copy of anything.

Evidence (4)
  • cluster/raft.go:Join
  • cluster/raft.go:Promote
  • TestJoinOnAnEmptyClusterIsActiveInOneCall
  • TestJoinIsRefusedWhileADatabaseExists

Audit logging is in the server and free.

But: It is off by default — turn it on in the configuration.

Evidence (2) Off by default
  • server/audit.go:AuditLogger
  • config/config.go:AuditConfig

Safe by default

On first start it generates a self-signed TLS keypair and serves over TLS.

But: The certificate is self-signed: a first client connection trusts it on first use and records its fingerprint. It is not a CA-issued certificate.

Evidence (2)
  • transport/transport.go:LoadOrCreateKeys
  • transport/transport.go:GenerateEd25519Keys

Authentication is always required. There is no flag that turns it off.

But: The CLI's -insecure flag is a client-side TLS verification bypass. It is not a server authentication switch, and there is no server-side equivalent.

Evidence (2)
  • server/auth.go:NewCredential
  • server/authz.go:authorize

The first start creates a root user from the password you choose and keeps only its hash in ./data/users.json; the password is never printed and never written to disk.

Evidence (3)

Passwords are hashed with Argon2id.

Evidence (1)
  • server/auth.go:NewCredential

Users, roles and per-database grants are persisted and enforced.

But: A role change applies at the next authentication, not to a connection that is already open.

Evidence (2)
  • server/authz.go:roleSatisfies
  • server/users.go:LoadUsersConfig

Status

What is not here

What it does not do yet is on Limitations. What is planned, and what is not planned, is on the Roadmap.