ntee-db

module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 29, 2026 License: Apache-2.0

README

ntee-db

A small, embedded, log-structured JSON key-value store written in pure Go, with prefix search and secondary indexes — usable from Go and Node.js.

An append-only JSONL file is the source of truth; an in-memory B-tree index serves fast lookups. No server, no separate process — the store runs inside your app, in the same spirit as lmdb or SQLite in embedded mode.

Highlights

  • Log-structured & crash-safe — the data log is the WAL; torn writes are detected and truncated on boot, and the index is always rebuildable.
  • Secondary indexes — string/number, multi-value, with exact / prefix / numeric-range queries, ±N first/last limits, and automatic per-value capping (MaxPerValue: keep only the newest N records per value).
  • Flexible index schema — add, drop, or change indexes between opens with no migration step; dropped indexes are soft-deleted (data preserved, recoverable), and Reindex back-fills new indexes over existing records (see below).
  • Prefix scans on the primary key; range delete by primary key for time-based pruning.
  • Fast boot via a persisted index snapshot (hint) + log-tail replay.
  • Disk-sized capacity, not RAM-sized — only keys + offsets are resident; values and large blobs stay on disk and are read on demand (hot reads served by the OS page cache). Unlike memory-bound stores such as Redis or memcached, the dataset is limited by disk, not RAM: a 1 GB-RAM device with a 40 GB disk can serve ~40 GB of data, as long as the key count fits the index budget (roughly key bytes + ~80 B of RAM per record). Degrades gracefully too — cold reads get slower instead of writes being refused.
  • Atomic int64 countersIncr/Decr with Redis-style semantics (missing key starts at 0, new value returned), plus bounded Topup/Take operators — Topup fills toward a max and returns how much didn't fit, Take subtracts only if the result stays above a floor — with the bound check and the update as one atomic operation, no read-then-write race. Counters are stored fixed-width and updated in place: a hot counter never grows the log and never creates compaction debt.
  • Single-writer safety via kernel flock (releases on any process exit). Unix (macOS/Linux) only.
  • Pure Go; the only dependency is tidwall/btree (MIT).

Packages

Directory What it is
nteedb-core/ The store itself — a Go library (package nteedb). Full design & API docs in its README.
nteedb-js/ ntee-db, the Node.js binding — the core compiled as a c-shared library (source in nteedb-js/capi/), loaded via FFI, shipped with prebuilt binaries.
nteedb-server/ A standalone TCP server (redis/memcached-style daemon): text protocol, single-line JSON responses, parallel reads, optional auth. Protocol docs in its README.
nteedb-client-js/ ntee-db-client, a pure-JS TCP client for the server — every command, pipelining, zero runtime dependencies.

(The JS binding stays server-free — it embeds the core directly; the JS client is its networked counterpart, talking to nteedb-server over TCP.)

Running the server

go run ./nteedb-server -schema nteedb-server/schema.example.json
$ nc 127.0.0.1 6666
put call:1 {"kind":"request"}
{"ok":true,"result":true}
ix kind request
{"ok":true,"result":["call:1"]}

Using from Go

go get github.com/nickooan/ntee-db
import nteedb "github.com/nickooan/ntee-db/nteedb-core"

db, err := nteedb.Open(nteedb.Options{Dir: "/path/to/store"})
if err != nil { /* ... */ }
defer db.Close()

db.Put("input:GetOrders", []byte(`{"q": "orders"}`))
v, ok, err := db.Get("input:GetOrders")
keys, err := db.PrefixScan("input:Get")

See nteedb-core/README.md for the full story: design, key ordering, secondary indexes, MaxPerValue capping, schema migration, and all options.

Using from Node.js

npm install ntee-db
import { NteeDB } from "ntee-db";

const db = NteeDB.open("/path/to/store", {
  indexes: [{ name: "traceId", kind: "string" }],
});

db.put("call:1", { kind: "request" }, { traceId: "T1" });
const rec = await db.get("call:1"); // parsed JSON
await db.secIndex("traceId", "T1"); // ['call:1', ...]
db.close();

Prebuilt binaries ship for darwin-arm64, linux-amd64, linux-arm64 — no Go toolchain needed at install time. See nteedb-js/README.md for the API, benchmarks vs lmdb/better-sqlite3, and notes.

Evolving the index schema

ntee-db treats the index set as a declaration, not a migration: change Options.Indexes (Go), the indexes open option (JS), or schema.json (server) between opens and the store adopts the new set — never rejected:

  • Dropped indexsoft-dropped: it stops being maintained and queryable, but its data is preserved in the records (a tombstone entry in meta.json tracks it, and Compact deliberately keeps the data). Re-declare the index before a Reindex and its surviving data comes back.
  • Added (or kind-changed) indexprospective: it covers records written from now on, but not history. DroppedIndexes() / ProspectiveIndexes() (server: dropped / prospective) report both states.
  • Reindex() (server: reindex, admin) resolves everything at once: it rewrites every live record, re-running each index's extractor (Go Extract func / JS & server jsonPath) over the old values — so a newly added derived index gets back-filled across all existing data — and purges soft-dropped leftovers from records and meta.json. Reads stay live while it runs; writes wait.

The one asymmetry: only extractor-based indexes can be back-filled. An explicit-values index (values passed per write) stays prospective — its historical values were never recorded anywhere to recover. If you expect to add an index over a field later, storing that field in the record and using jsonPath/Extract keeps that door open.

Day-to-day writes never pay for any of this: extraction runs once at write time, the result is persisted in the record, and boots/compactions rebuild indexes from those persisted values without re-running extractors. Details in nteedb-core/README.md.

Building the server

The server is pure Go (no cgo), so every platform cross-compiles from any host in seconds — no Docker:

./build.sh                 # → bin/nteedb-server-{darwin-arm64,linux-amd64,linux-arm64}
./build.sh linux-arm64     # one target

Linux builds are fully static — scp and run, no runtime dependencies.

Development

go test -race ./...            # core + capi + server tests
./build.sh                     # server binaries (pure Go, cross-compiles)
nteedb-js/capi/build.sh        # JS native libs (macOS host + Linux via Docker)
cd nteedb-js && npm test       # Node binding tests
cd nteedb-client-js && npm test # TCP client tests (spawns a real server; needs Go)

License

Apache-2.0

Directories

Path Synopsis
Package nteedb is a pure-Go, dependency-free embedded key-value store.
Package nteedb is a pure-Go, dependency-free embedded key-value store.
nteedb-js
capi command
Command capi builds nteedb as a C-shared library (-buildmode=c-shared) so it can be loaded in-process from other languages (e.g.
Command capi builds nteedb as a C-shared library (-buildmode=c-shared) so it can be loaded in-process from other languages (e.g.
Command nteedb-server exposes a ntee-db store over TCP with a memcached-style text protocol and single-line JSON responses.
Command nteedb-server exposes a ntee-db store over TCP with a memcached-style text protocol and single-line JSON responses.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL