ntee-db

module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 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.
  • Per-key TTL, lazily enforced — an optional ttl on any write; expiry lives in the primary-key index (zero-cost check at lookup), expired keys read as missing and are reaped in the background, and compaction drops leftovers. On counters the ttl applies on create only, making Incr(key, 1, time.Minute) a complete fixed-window rate limiter.
  • 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