Lantern is an in-memory database for live relationships: typed values live at keys, directed weighted edges connect them, and both can expire. Read a value, query its neighborhood, and search its content through the same API.
With Git and Docker (including Compose), start the published stack:
git clone --depth 1 https://github.com/anaregdesign/lantern.git
cd lantern/deploy/compose
docker compose up -d --pull always --waitNo local build or SDK install is needed. Once startup completes, open
http://localhost:8080/cli. Check that Gateway is
http://localhost:6380 (use Reset if you have saved another gateway).
In a fresh demo, paste this block once into the CLI command input;
commands run in order automatically:
put vertex user:42 "alice"
put vertex item:7 "desk lamp"
get vertex item:7
add edge user:42 item:7 1.5
add edge user:42 item:7 0.5
get edge user:42 item:7
search lamp
bfs user:42 1 10
The vertex read shows desk lamp, the edge read shows weight 2, and search
finds item:7. The final BFS displays both vertices and their directed edge
in the graph canvas. You can also explore Data → Vertices and the Ops
page in the browser. Pasting the block again adds another 2 to the edge.
The stack includes three server replicas, Admin, MCP, and Prometheus. It uses
host ports 6380–6382, 8080, 6390, and 9091; keep them free for this
demo. This demo uses HTTP without auth; use a trusted development network.
The Compose guide covers configuration.
Stop the stack from the same directory:
docker compose downThis keeps the backup volumes for your next run. To discard the demo's stored
snapshots as well, use docker compose down -v. Prefer an HTTP-only trial?
See Try with curl.
Cache semantics apply to the graph itself. Use Lantern beside your system of record when the question is “what is related to this key right now?”
| Capability | What it gives you |
|---|---|
| Key/value vertices | Read and write typed values by key, with optional TTL. No schema registration is required. |
| Additive, expiring edges | Each event can add its own weighted contribution and expiration to a relationship. The current weight is the sum of live contributions. |
| Server-side graph queries | Bounded BFS, seed-local Personalized PageRank, and local community discovery return graph views. Optional spanning-tree or shortest-path-tree reduction runs on the server. |
| Full-text search in the same store | BM25-ranked vertex search supports phrase, fuzzy, prefix-term, and match-mode options; combine search results with graph queries. |
| One protocol surface | Connect JSON/protobuf, gRPC, and gRPC-Web share a listener. Use Go, TypeScript, Dart, Rust, the CLI, or plain HTTP. |
The data model is a weighted directed graph: a vertex is a key and typed value; an edge connects an ordered pair of keys. Edges have weights and expiration, rather than relationship labels or arbitrary property bags. Your application owns key namespaces and the meaning of weights.
graph LR
u(("user:42<br/>alice")) -- "live weight: 2" --> i(("item:7<br/>desk lamp"))
AddEdge appends a contribution; PutEdge replaces the edge. With TTL-bearing
positive contributions, recent events accumulate and older ones fall out.
Assume both endpoint vertices remain live throughout this example:
t=0 add a → b, weight=1, TTL=3s → live weight 1
t=1 add a → b, weight=1, TTL=3s → live weight 2
t=3 first contribution expires → live weight 1
t=4 last contribution expires → no live edge
Expiration is a step change, not continuous decay. SDK AddDecayingEdge
helpers approximate a geometric decay curve with staggered expirations.
Reads exclude expired state; background GC reclaims it. TTL is optional per
write: omitting expiration means permanent state for the lifetime of the graph.
An edge is visible only while both endpoint vertices are live. Edge writes
auto-create missing endpoints but do not extend an existing live vertex's TTL.
The Illuminate RPC selects a traversal family and returns the resulting
vertices and edges. The CLI exposes those families directly:
| Query | Example | Use it for |
|---|---|---|
| Bounded BFS | bfs user:42 2 10 |
Explore two hops with bounded fan-out. |
| Personalized PageRank | pagerank user:42 10 |
Rank nearby candidates as a star whose edge weights are PPR scores. |
| Local community | community user:42 30 |
Find a related local group. |
Choose raw, TF-IDF, or BM25 graph weighting, and an optional key-prefix filter.
BFS and community also support mst / spt reduction with a min/max objective.
See the Go example, query complexity guide,
and search contract for behavior and bounds. Graph proximity
comes from stored relationships; it is distinct from embedding similarity.
- Recommendations and personalization: user → item events with independent TTLs, plus neighborhood queries for current candidates.
- Fraud and abuse signals: account, device, and IP co-occurrences that accumulate during a time window.
- Trends and online graph features: recent interactions available to request-time queries instead of waiting for a batch graph rebuild.
- Shared session or agent context: short-lived relationships and facts; the optional MCP server exposes presence, claims, activity, and a blackboard over Lantern.
Before choosing it, account for these boundaries:
| Boundary | Deployment consequence |
|---|---|
| In-memory by default | A restart loses the graph unless you configure recovery. Graph-only snapshots lose writes since the last snapshot; opt-in durable receipt-WAL mode has a separate WAL/backup recovery contract. See backup and restore. |
| Full replicas, no sharding | Every HA replica holds the entire graph. The working set must fit in one process's RAM. |
| Eventual consistency | Leaderless replication provides HA, with asynchronous propagation between replicas. It is not linearizable; WAN replication is outside the supported scope. See the replication contract. |
| Local graph queries | Seed-local traversal and ranking are supported; whole-graph offline analytics are outside the intended workload. |
| Authentication and authorization | Authentication defaults to OFF. OIDC enables Role-only prefix Allow/Deny and named machine credentials; HA uses a separate signed-membership mTLS workload plane. See ADR 0012 for the qualification boundary. |
| Pre-v1 API | Wire schemas, SDK APIs, CLI grammar, env vars, and metrics may break between releases. Pin versions and review upgrades. |
| Client | Install | Guide |
|---|---|---|
| Go | go get github.com/anaregdesign/lantern/sdks/go |
Go SDK |
| TypeScript / Node | npm install lantern-sdk |
Node and browser SDK |
| Dart / Flutter | dart pub add lantern_client |
Dart SDK |
| Rust | cargo add lantern-client |
Rust SDK |
SDKs are independently released and differ in supported surface. Their guides define transport, auth, retry, and receipt contracts.
The Go SDK is its own module — external projects pull only Connect-Go and protobuf, nothing from the server:
go get github.com/anaregdesign/lantern/sdks/goimport "github.com/anaregdesign/lantern/sdks/go"
cli, err := client.NewLantern("http://localhost:6380")
if err != nil { log.Fatal(err) }
defer cli.Close()
ctx := context.Background()
// Vertices accept string, int, float, bool, time.Time, time.Duration, []byte, nil.
for _, input := range []struct{ key string; value any }{
{"user:42", "alice"},
{"item:7", "desk lamp"},
} {
outcome, err := cli.PutVertex(ctx, input.key, input.value, time.Hour)
if err != nil || outcome != client.PutOutcomeAppliedAndLive {
log.Fatalf("PutVertex %q: outcome=%s err=%v", input.key, outcome, err)
}
}
// Each AddEdge appends a contribution with its own TTL and returns the live sum.
_, _ = cli.AddEdge(ctx, "user:42", "item:7", 1.0, 30*time.Minute)
// Approximate geometric decay with staggered TTL contributions (one AddEdges RPC).
_, _ = cli.AddDecayingEdge(ctx, "user:42", "item:7",
client.DecayOpts{InitialWeight: 16, Ratio: 0.5, Steps: 5, Interval: time.Minute})
// Walk: 2 hops, top-3 per hop, TF-IDF weighted.
g, _ := cli.Illuminate(ctx, "user:42",
client.WithBFS(client.BFSOpts{Step: 2, FanOut: 3}),
client.WithWeighting(client.WeightingTFIDF))
// Full-text search: BM25-ranked hits over vertex content.
hits, _ := cli.SearchVertices(ctx, "desk lamp", client.WithMatchMode(client.MatchAll))
// Page beyond the per-call limit without collecting an unbounded slice.
for hit, err := range cli.SearchVerticesIter(ctx, "desk lamp",
client.WithSearchLimit(50), client.WithFullVertex()) {
if err != nil { log.Fatal(err) }
fmt.Println(hit.Key, client.StringValue(hit.Vertex))
}
// Prefix scan: enumerate a namespace, auto-paginated.
for batch, err := range cli.ScanVerticesAll(ctx, "user:", 100) {
if err != nil { log.Fatal(err) }
for _, v := range batch { fmt.Println(client.StringValue(v)) }
}Use client.WithAuthToken for bearer auth, client.WithRetry for eligible
reads and unconditional Put, or client.NewLanternFailover for opt-in static
endpoint failover. Receipt-less Add and exact/prefix Delete make one attempt;
receipt-backed exact mutations use a separate same-endpoint recovery contract.
See the SDK guide and
complete example before choosing retry policies.
Ships to npm as lantern-sdk (ESM + CJS, bundled types,
Node 20+):
npm install lantern-sdkimport { Objective, Reduction, Weighting, connect } from "lantern-sdk";
const client = connect("http://localhost:6380");
try {
await client.putVertex({ key: "user:42", value: "alice", ttlSeconds: 3600 });
await client.putVertex({ key: "item:7", value: "desk lamp", ttlSeconds: 3600 });
await client.addEdge({ tail: "user:42", head: "item:7", weight: 1.0, ttlSeconds: 1800 });
const bfs = await client.illuminate("user:42", {
bfs: {
step: 2,
fanOut: 16,
reduction: Reduction.SHORTEST_PATH_TREE,
objective: Objective.MAXIMIZE,
},
weighting: Weighting.TFIDF,
});
const pagerank = await client.illuminate("user:42", {
ppr: { topN: 16, restartProb: 0.15, epsilon: 0.0001 },
});
const community = await client.illuminate("user:42", {
community: { maxSize: 16, restartProb: 0.15, epsilon: 0.0001 },
});
const hits = await client.searchVertices("desk lamp", { limit: 10 });
console.log(bfs.vertices.size, pagerank.vertices.size, community.vertices.size);
for await (const page of client.scanVerticesAll("user:", 500)) {
for (const v of page) console.log(v.key);
}
} finally {
client.close();
}JS values map to typed proto fields (string, number, bigint,
boolean, Date, Uint8Array, null, plus explicit numeric wrappers);
ordinary batch writes auto-chunk, while opt-in receipt batches remain
unsplit. The browser build (lantern-sdk/web) is what powers the
admin SPA. Full API:
sdks/node/README.md.
Published Node 0.13.0 includes targeted contribution Delete alongside
the opt-in receipt APIs. See the SDK guide for safe mutation replay.
lantern_client is a pure-Dart,
Android/iOS-first SDK with immutable values, TTL-preserving Graph results,
CRUD, scans, search, typed traversal, degree ranking, and explicit status.
Its transport owns secure endpoints, token providers, deadlines, cancellation,
bounded retry, typed failures, and auth-exempt Health probing. Published
0.4.1 includes opt-in receipts and targeted contribution Delete. Start with
the SDK guide and maintained Flutter example.
lantern_client_offline is
an experimental, opt-in Repository layer for cached snapshots, pending
overlays, and foreground replay. Published 0.5.0 adds targeted contribution
Delete to receipt-backed conditional Vertex Put, exact Vertex/Edge Delete,
and explicit-ID Edge Add. It injects a transactional store, keeps credentials
application-owned, respects TTL, and reconciles possibly dispatched writes
status-first. See the offline guide for the
qualified release contract. The in-memory reference store is test
infrastructure; the SQLite adapter remains
a separate unpublished Flutter package.
lantern-client targets native Tokio
applications on Linux, macOS, and Windows. Its single-endpoint client supports
trusted HTTPS, private CA/mTLS, per-attempt bearer auth, Health, exact-value/TTL
CRUD, bounded scans/search, typed traversal, degree ranking, and explicit
status. Generated Tonic/Prost clients stay private; building the published crate
needs no proto sources or system protoc. See the crate guide
and complete secure example.
Use Connect JSON as in the quickstart, or generate bindings from proto/graph/v1/graph.proto with buf, protoc, or a Connect codegen plugin. The same listener accepts Connect, gRPC, and gRPC-Web; Connect JSON also works over HTTP/1.1.
Install a lantern-cli binary from a root release,
or use brew install --cask anaregdesign/tap/lantern-cli on macOS. The CLI and
interactive REPL support verb-first commands; Admin web /cli offers common
CRUD, search, and traversal commands in the browser:
lantern-cli put vertex user:42 "alice" 3600
lantern-cli put vertex item:7 "desk lamp" 3600
lantern-cli add edge user:42 item:7 1.0 1800
lantern-cli bfs user:42 2 10 weighting=tfidf
lantern-cli pagerank user:42 10
lantern-cli community user:42 30
lantern-cli search "desk lamp" mode=all limit=20
lantern-cli replUse lantern-cli <cmd> --help for flags and help bfs|pagerank|community
inside the REPL for traversal defaults, bounds, and examples. Reads emit JSON
by default; search also supports NDJSON and TSV. The Admin UI
adds graph visualization, data browsing, search, and operational status.
You need Docker, curl, and a POSIX shell. No checkout, build, or SDK install is required. Start a released server in one terminal:
docker run --rm --name lantern-demo \
-p 127.0.0.1:6380:6380 \
ghcr.io/anaregdesign/lantern:v0.35.4Once the server starts, paste this into a second terminal:
rpc() {
curl -fsS -w '\n' \
-H 'Content-Type: application/json' \
-H 'Connect-Protocol-Version: 1' \
-d "$2" \
"http://localhost:6380/graph.v1.LanternService/$1"
}
# Store ordinary values at keys.
rpc PutVertex '{"vertex":{"key":"user:42","string":"alice"}}'
rpc PutVertex '{"vertex":{"key":"item:7","string":"desk lamp"}}'
rpc GetVertex '{"key":"item:7"}'
# Two events contribute to the same directed edge: 1.5 + 0.5 = 2.
rpc AddEdge '{"edge":{"tail":"user:42","head":"item:7","weight":1.5}}'
rpc AddEdge '{"edge":{"tail":"user:42","head":"item:7","weight":0.5}}'
rpc GetEdge '{"tail":"user:42","head":"item:7"}'
# Query the neighborhood and search the stored content.
rpc Illuminate '{"seed":"user:42","bfs":{"step":1,"fanOut":10}}'
rpc SearchVertices '{"query":"lamp","limit":10}'You should see PUT_OUTCOME_APPLIED_AND_LIVE for both writes, "desk lamp"
in the vertex read, and an edge weight of 2. Illuminate returns both
vertices and their connecting edge; search returns a hit for item:7.
This demo uses local HTTP without auth and omits expiration, so its values
remain until deleted or the container stops. If port 6380 is occupied, change
the host port in -p and the matching port in the rpc URL. To stop and discard
the demo:
docker stop lantern-demoThe protobuf contract is the full API reference. Exact reads/writes/deletes have singular and plural forms:
| Surface | Operations |
|---|---|
| Vertices | Get, Put with TTL/conditional outcomes, exact Delete |
| Edges | Get live weight, additive Add, replacement Put, whole-edge Delete, targeted contribution Delete |
| Enumeration | Cursor-paginated vertex/key/edge scans, prefix count, capped prefix Delete with dry-run |
| Queries | Search, Illuminate traversal families, degree ranking |
| Operations | Health, server/replication status, backup stream, CDC Subscribe/Snapshot, receipt capability/status |
The source candidate also provides receipt-required mixed graph mutations: unconditional Vertex Puts followed by explicit-ID Edge Adds, with one local atomic publication and per-effect HA conflict resolution.
Add is not an idempotent write. A blind retry after a lost response can add a contribution twice. Supported SDKs provide separate opt-in receipt APIs for bounded, same-endpoint recovery of exact mutations. Receipt-bearing groups remain unsplit; Put Edge and prefix Delete have no receipt path. Persist an explicit contribution ID before Add if you need to retract that contribution later. See bounded mutation receipts for identity, continuity, status, and retry rules, and each SDK guide for its published support.
From a repository checkout:
cd deploy/compose
docker compose up -d --pull alwaysOpen http://localhost:8080. The default starts one OFF Server on loopback
6380 and Admin. HA requires the explicit signed-membership overlay; MCP and
Prometheus are opt-in local profiles. See the
Compose guide for configuration and cleanup.
For OIDC, supply one human configuration
for provisioning, one-time initialization and every start. Identity keys and
historical trust stay under material_root; native state stays under separate
persistence_root. Neither stores a replacement runtime configuration. Current
Issuer changes, including later additions, are reconciled before public
readiness without granting users or resetting policy. Operator signing custody
and membership renewal remain separate. Browser-only clients need no API audience;
an absent API profile rejects external API JWTs.
helm install lantern deploy/helm/lanternThe chart defaults to one OFF Server. Explicit HA uses a StatefulSet, signed private workload membership, anti-entropy reconciliation, and a PodDisruptionBudget. See chart values and the HA runbook for topology, readiness, partitions, rolling upgrades, and recovery.
Leave LANTERN_PEER_* unset for a single instance. Choose your recovery mode
using the backup guide. Server, Admin, and MCP images are
published to GHCR with multi-arch builds and cosign signing; the server image
is ghcr.io/anaregdesign/lantern.
Configuration uses LANTERN_* env vars. The complete generated reference
defines defaults, validation, limits, and recovery requirements. Common entries:
| Variable | Default | Purpose |
|---|---|---|
LANTERN_PORT |
6380 |
RPC listener |
LANTERN_GC_INTERVAL_SECONDS |
60 |
Background cleanup interval |
LANTERN_MAX_VERTICES / LANTERN_MAX_EDGES |
0 (unlimited) |
Graph-admission soft caps; size causal retention budgets and GOMEMLIMIT too |
LANTERN_AUTH_MODE |
off |
oidc requires complete native security/Issuer configuration; retired static-token settings are rejected |
LANTERN_TLS_CERT_FILE / LANTERN_TLS_KEY_FILE |
unset | TLS; LANTERN_TLS_CLIENT_CA_FILE enables mTLS |
LANTERN_CORS_ALLOWED_ORIGINS |
empty | Browser origin allow-list |
LANTERN_RECEIPT_WAL_MODE |
graph-only |
Default volatile runtime, or configured fresh / restart durable receipt runtime |
LANTERN_METRICS_ADDR |
:9090 |
Prometheus and HTTP health listener |
LANTERN_STRICT_CONFIG |
false |
Reject malformed/unknown config when enabled |
For private developer TLS, OIDC providers and Google/Apple OAuth clients, start with the placeholder configuration files. Keep actual endpoint/client selections, downloaded provider files and keys outside Git.
Prometheus metrics, structured JSON logs, gRPC Health and HTTP
/healthz / /readyz, optional OpenTelemetry tracing, and gRPC reflection
are available. See the observability guide for setup
and the HA runbook for operational signals.
flowchart LR
C["SDKs · CLI · Admin · MCP"] --> S["Connect / gRPC / gRPC-Web"]
S --> G["In-memory vertices + edges"]
S --> I["BM25 search index"]
S <--> R["Replication and mutation log"]
R <--> P["Full peer replicas (optional HA)"]
The monorepo contains six Go modules joined by go.work, independent
Dart/Rust SDKs, and Bun-managed TypeScript packages. The server depends on
pb and core, independently of client SDKs.
| Path | Role |
|---|---|
proto/ / pb/ |
Shared schema / generated Go protobuf and Connect stubs |
core/ |
Graph, cache, collections, concurrency, and NLP building blocks |
server/ |
Data/replication services, internal authentication adapters, bounded search/paging, and runtime assembly |
sdks/ |
Independently consumable client packages |
cli/ / admin/ / mcp/ |
CLI, browser console, and agent-facing MCP server |
deploy/ |
Compose stack and Helm chart |
docs/ |
Operational guides and architecture decisions |
Start with CONTRIBUTING.md for issue triage, testing, and release rules, and AGENTS.md for module boundaries and codegen. Tests run in each Go submodule as well as the root; Dart, Flutter, Node, and Rust have their own gates.
MIT.