Embedding immudb in-process (as a Go library)

July 30, 2026 · View on GitHub

immudb can run entirely inside your Go process — no server binary, no container, no network. You import a package, open a local data directory, and call methods directly. This is ideal for embedding immudb into another application (a CLI, a desktop app, a plugin, …).

The immudb server is just one consumer of these same packages. Everything the server does with your data, it does through pkg/database, which in turn builds on the embedded/* engines.

Choosing a layer

There are two practical ways to embed, depending on how much you want and how lean a dependency graph you need.

LayerImportGives yougRPC pulled in?
A. pkg/database (recommended)github.com/codenotary/immudb/pkg/databaseHigh-level DB: verifiable Set/Get, SQL, documents, transactions, proofs — the exact code path the server usesLinks the gRPC library (for typed codes/status errors and the schema types), but never starts a server or opens a socket
B. embedded/store (+ embedded/sql) (leanest)github.com/codenotary/immudb/embedded/store, .../embedded/sqlAppend-only key-value store and SQL engine; you build your own API and call the stateless store.Verify* proof helpersNo gRPC at all, no protobuf — just google/uuid and Prometheus

Recommendation: start with Layer A (pkg/database). It gives you immudb's core value — tamper-evident, cryptographically verifiable KV + SQL — with the least code. Drop to Layer B only if you want the absolute minimum dependency footprint and are happy to define your own API surface.

A third option exists for tests — pkg/server/servertest.NewBufconnServer runs the full server (auth, sessions, multi-DB) over an in-memory buffer and talks to it with the standard pkg/client. It links the entire server stack, so it is not recommended for embedding; it is listed here only for completeness.

log := logger.NewSimpleLoggerWithLevel("embedded", os.Stdout, logger.LogError)
opts := database.DefaultOptions().WithDBRootPath("./data")

// NewDB creates the directory + files; OpenDB opens an existing one.
// Pass a nil sql.MultiDBHandler for a single, self-contained database.
db, err := database.NewDB("mydb", nil, opts, log)
// ...
defer db.Close()

ctx := context.Background()

// verifiable write + read
db.VerifiableSet(ctx, &schema.VerifiableSetRequest{
    SetRequest: &schema.SetRequest{
        KVs: []*schema.KeyValue{{Key: []byte("k"), Value: []byte("v")}},
    },
})
entry, _ := db.VerifiableGet(ctx, &schema.VerifiableGetRequest{
    KeyRequest: &schema.KeyRequest{Key: []byte("k")},
})

// embedded SQL
db.SQLExec(ctx, nil, &schema.SQLExecRequest{Sql: "CREATE TABLE t (id INTEGER, PRIMARY KEY id)"})
reader, _ := db.SQLQuery(ctx, nil, &schema.SQLQueryRequest{Sql: "SELECT id FROM t"})
defer reader.Close()

DB methods take schema.* request structs — the same messages the gRPC API uses — which makes them verbose but complete. Full runnable program: examples/embedded/main.go.

Key constructors: database.NewDB / database.OpenDB (pkg/database/database.go), options via database.DefaultOptions() (pkg/database/dboptions.go).

Layer B — embedded/store + embedded/sql (leanest)

st, _ := store.Open("./data", store.DefaultOptions())
defer st.Close()

tx, _ := st.NewWriteOnlyTx(ctx)
tx.Set([]byte("k"), nil, []byte("v"))
tx.Commit(ctx)

valRef, _ := st.Get(ctx, []byte("k"))
val, _ := valRef.Resolve()

Verifiable, runnable examples live next to the packages as Go example tests:

SQL requires multi-indexing

sql.NewEngine needs the store opened with multi-indexing enabled, otherwise it returns ErrMultiIndexingNotEnabled:

st, _ := store.Open("./data", store.DefaultOptions().WithMultiIndexing(true))
engine, _ := sql.NewEngine(st, sql.DefaultOptions().WithPrefix([]byte("sql")))

(Layer A's NewDB/OpenDB already sets this for you.)

Important: single-writer — immudb does not lock the data directory

A data directory must be opened by one process at a time. Within that process the store is safe for concurrent use by multiple goroutines.

immudb does not enforce this for you. There is no lock file and no flock: calling store.Open (or NewDB/OpenDB) on a directory another live process already has open succeeds, and both processes will then append to the same transaction log and index. The result is silent corruption — the very failure this constraint exists to prevent. Nothing will warn you.

Mutual exclusion is the embedder's responsibility. Either:

  • give each process its own directory;
  • funnel all access through a single long-lived process; or
  • take your own cross-process lock around opening the store.

For a worked example of the last option, see contrib/immuledger/internal/ledger/lock_unix.go, which takes an advisory flock on a sidecar lock file before opening the store and releases it on close.

Data directory layout

Point the store at any path via WithDBRootPath (Layer A) or the first argument to store.Open (Layer B). immudb creates its append-only logs, index, and hash-tree files under that directory. Back it up by copying the directory while the process is stopped (or use immudb's export/replication APIs for online copies).

To prove an embed does not pull in the gRPC server:

go list -deps ./yourpkg | grep immudb/pkg/server   # should print nothing