README
¶
Lantern — the in-memory Key-Vertex-Store
Lantern is a cache that understands relationships. It is a
Key-Vertex-Store: you use it like the key-value cache you already run —
put, get, TTL — but every value is a vertex, and weighted, decaying
edges connect them. So alongside "get me this value", your request-path
code can ask, in a single millisecond-scale RPC:
- "What's near this key right now?" —
illuminatewalks the live graph and returns a subgraph already shaped for your use case (k-NN, spanning tree, shortest paths, PageRank, community). - "How strongly are these two related at this moment?" — edge weights are live sums of TTL'd contributions, so relationship strength decays on its own as events age out.
- "Which keys match these words?" — BM25 full-text search over vertex content, with fuzzy/phrase/prefix matching, built into the same store.
No nightly graph pipeline, no heavyweight graph database on the hot path, no fetching a wall of edges to post-process in your service. The graph lives where cache data lives: in memory, with TTLs, in front of your system of record.
> put vertex user:42 "alice"
> put vertex item:7 "lamp"
> add edge user:42 item:7 1.0 1800 # each event appends a decaying contribution
> illuminate user:42 2 10 # 2 hops out, top-10 per hop — one RPC
{ "vertices": { ... }, "edges": { ... } }
Those three writes just built a live graph:
graph LR
u(("user:42<br/>alice")) -- "1.0 · ttl 30m" --> i(("item:7<br/>lamp"))
Everything speaks the Connect protocol — with gRPC and gRPC-Web wire compatibility on the same h2c socket — so Go services, browsers, CLIs, and anything with a protobuf toolchain hit the identical API on one port.
Status: pre-1.0 — expect breaking changes. Until
v1.0.0, Lantern makes no backward-compatibility guarantees: the proto/wire schema, SDK APIs, CLI grammar,LANTERN_*env vars, and metric names can change between releases. Pin a version if you need stability.
Why a graph in your cache?
Most graph stores are built for offline analytics over yesterday's snapshot. Most caches flatten relationships away entirely. Lantern sits in the gap: online, behavioral graph data with cache semantics.
Never used a graph? It's just entries + connections
A key-value cache stores isolated entries. Lantern turns those entries into a graph in the mathematical sense — the weighted directed graph of graph theory, G = (V, E, w): vertices, directed edges between them, a real-valued weight on each edge, and (Lantern's cache-native twist) a TTL on all of it. Not the property graph of graph databases, with typed, attribute-laden edges — the plain mathematical object. Same data, one new dimension:
flowchart TB
subgraph before["A classic KVS — every entry is an island"]
a1["user:42 = alice"]
a2["user:99 = bob"]
a3["item:7 = lamp"]
a4["item:9 = desk"]
end
subgraph after["A Key-Vertex-Store — the same entries, connected"]
direction LR
u42(("user:42<br/>alice")) -- "1.5" --> i7(("item:7<br/>lamp"))
u42 -- "0.3" --> i9(("item:9<br/>desk"))
u99(("user:99<br/>bob")) -- "2.0" --> i7
end
before -->|"add edge …"| after
That's the entire vocabulary you need:
- Vertex — one cache entry: a key and its value (
user:42 = alice), with a TTL. - Edge — an ordered pair of keys with a weight and a TTL: one element of E, nothing more — no type, label, or property bag. If you need relationship kinds ("clicked" vs "bought"), encode them in your key design or weight conventions.
- Weight — how strong that link is right now. In Lantern it's the live sum of decaying contributions, so recent events count more than old ones.
- TTL — everything above expires on its own; nothing needs a cleanup job.
Staying with the mathematical object is deliberate — it is what keeps the rest of Lantern simple and fast:
- The classic algorithms apply directly. Spanning trees, shortest
paths, PageRank, community detection — graph theory defines them on
exactly this object, a weighted directed graph.
illuminateruns them natively over every edge, with no "which relationship types does this walk follow?" configuration and no schema the server has to know about; a property graph has to be flattened down to weights before any of that theory applies. - Every event can pile onto the same edge. The additive, decaying weight model works because merging contributions is just addition — there are no per-type aggregation rules to define.
- Nothing to design up front, nothing to migrate. A new kind of event starts flowing into the graph the moment you write it — cache semantics extend to the data model itself.
- Edges stay tiny. A contribution is a weight and an expiration, which is why a large working set fits in one process's memory in the first place.
The payoff: questions like "what has this user interacted with lately, and how strongly?" stop being JOINs over event logs in your warehouse and become a one-RPC lookup against the cache.
TTLs on everything — including edges
Every vertex and every edge can carry its own expiration. A background janitor compacts expired entries and prunes edges whose endpoints vanished. The working set stays warm and small without manual deletes: the graph forgets old information the same way real-world relationships fade.
Edge weights that accumulate — and decay
Edges are not single scalars. Each add edge appends another
contribution with its own TTL; the reported weight is the live sum of
contributions that have not yet expired:
t=0 add edge a b 1.0 ttl=3s → weight(a,b) = 1
t=1 add edge a b 1.0 ttl=3s → weight(a,b) = 2 (two contributions live)
t=3 first contribution expires → weight(a,b) = 1
t=4 second expires → weight(a,b) = 0 (edge gc'd)
xychart-beta
title "weight(a,b) over time — two 1.0 contributions, each ttl=3s"
x-axis "seconds" [0, 1, 2, 3, 4]
y-axis "live weight" 0 --> 2.5
line [1, 2, 2, 1, 0]
This is the model behavioral signals actually want: every click, view, or
co-occurrence is one append, and "how strong is this relationship right
now" falls out of the math — no batch job. Need classic idempotent
replace instead? That's put edge.
Vertices also auto-materialize on edge writes (inheriting the edge's TTL), so ingesting an event stream is just a stream of edge writes.
Graph queries as single RPCs
One Illuminate call walks the live graph from a seed and returns exactly
the shape you asked for. Three orthogonal axes select it:
| Axis | Options | What it picks |
|---|---|---|
algorithm |
none (default) / mst / spt / ppr / community |
Raw k-NN subgraph, spanning tree, shortest-path tree, Personalized PageRank neighbourhood, or the seed's natural community (conductance-cut, returned as a real induced subgraph) |
objective |
max (default) / min |
Keep strongest edges vs cheapest edges — the direction of both the per-hop top-k prune and the tree reduction (ignored by ppr, which ranks by mass) |
weighting |
raw (default) / tfidf / bm25 |
Edge-weight transform applied before the walk — TF-IDF and BM25 damp hub vertices like "popular" items |
Seeing is believing. Say the store holds this graph (labels are weights):
graph LR
a((a)) -- 1 --> b((b))
a -- 1 --> c((c))
b -- 2 --> d((d))
b -- 3 --> e((e))
c -- 1 --> e
c -- 4 --> f((f))
illuminate a 2 2 algorithm=spt objective=max treats heavy edges as cheap
(cost = 1/weight), so one RPC hands back the strongest-relationship
tree — no client-side post-processing:
graph LR
a((a)) -- 1 --> b((b))
a -- 1 --> c((c))
b -- 3 --> e((e))
c -- 4 --> f((f))
Flip to objective=min and weights become costs: the same RPC now returns
the cheapest-path tree, reaching e via c (cost 1+1) instead of via
b (cost 1+3):
graph LR
a((a)) -- 1 --> b((b))
a -- 1 --> c((c))
b -- 2 --> d((d))
c -- 1 --> e((e))
A few more combinations and what they buy you:
illuminate user:42 2 10 # raw 2-hop neighbourhood
illuminate user:42 3 8 algorithm=spt objective=max # most-relevant path tree
illuminate user:42 3 8 algorithm=mst objective=min # clustering / dedup backbone
illuminate user:42 2 10 algorithm=ppr # PageRank-ranked neighbourhood
illuminate user:42 2 10 algorithm=community # the seed's natural community
illuminate user:42 2 10 weighting=tfidf # suppress hub items
PPR takes two locality knobs (restart_prob, epsilon — higher restart
keeps the walk closer to the seed; smaller epsilon pushes further for more
recall). A prefix= filter restricts the walk to a key namespace during
traversal, yielding the prefix-induced subgraph — note that with mst/spt
a matching vertex reachable only through a non-matching bridge is excluded,
because the bridge is not traversable.
Full-text search over the same store
SearchVertices runs relevance-ranked (BM25) full-text search over vertex
content — key plus value — as the content-addressed counterpart to prefix
scans. Ranked hits make natural seeds for a follow-up illuminate:
lantern-cli search "rolling update" # OR-union of the query words
lantern-cli search "rolling update" --mode all # require every word (AND)
lantern-cli search "rolling update" --phrase # adjacent, in order
lantern-cli search serach --fuzziness 1 # typos still hit
lantern-cli search lan --prefix-terms # "lan" finds "lantern"
lantern-cli search espresso --prefix user. # scope to a key namespace
The index is maintained server-side and is on by default
(LANTERN_SEARCH_ENABLED).
Try it in 60 seconds
Start a server (Docker, Homebrew, or source):
docker run --rm -p 6380:6380 ghcr.io/anaregdesign/lantern:latest
# or on macOS:
brew tap anaregdesign/tap
brew install --cask lantern # server (binary: lantern)
brew install --cask lantern-cli # client (binary: lantern-cli)
# or from source:
go run ./server/cmd # listens on :6380
Then poke at it with the CLI — lantern-cli repl for an interactive prompt,
or the same grammar as verb-first one-liners:
$ lantern-cli repl
> put vertex alice Alice
OK (1.2ms)
> put vertex bob Bob 3600 # third arg = TTL seconds
OK (0.9ms)
> add edge alice bob 1.5 3600 # additive: appends a contribution
OK (1.1ms)
> add edge alice bob 0.5 3600 # second contribution
OK (0.8ms)
> get edge alice bob # live sum of unexpired contributions
2.000000
OK (0.6ms)
> illuminate alice 2 5
{
"vertices": { ... },
"edges": { ... }
}
OK (2.3ms)
Prefer a UI? One docker compose up brings up a 3-replica HA cluster, the
browser Admin console, and Prometheus — see Deploying.
Use it from your language
Go
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/go
import "github.com/anaregdesign/lantern/sdks/go"
cli, err := client.NewLantern("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.
_ = cli.PutVertex(ctx, "user:42", "alice", 1*time.Hour)
_ = cli.PutVertex(ctx, "item:7", "lamp", 1*time.Hour)
// 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)
// 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))
// 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)) }
}
Operational tiers compose in: client.WithAuthToken for bearer-token
servers, client.WithRetry for opt-in full-jitter retries (applied only to
RPCs that are idempotent under your configuration), and
client.NewLanternFailover for sticky-cursor rotation across HA replicas.
Full worked example: sdks/go/example/main.go.
TypeScript / Node
Ships to npm as lantern-sdk (ESM + CJS, bundled types,
Node 20+):
npm install lantern-sdk
import { connect } from "lantern-sdk";
const client = connect("http://localhost:6380");
try {
await client.putVertex({ key: "user:42", value: "alice", ttlSeconds: 3600 });
await client.addEdge({ tail: "user:42", head: "item:7", weight: 1.0, ttlSeconds: 1800 });
const graph = await client.illuminate("user:42", { step: 2, k: 16 });
const hits = await client.searchVertices("desk lamp", { limit: 10 });
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);
batch writes auto-chunk. The browser build (lantern-sdk/web) is what
powers the admin SPA. Full API:
sdks/node/README.md.
Anything else
Generate bindings from
proto/graph/v1/graph.proto with buf, protoc,
or any Connect codegen plugin. The server
multiplexes Connect (JSON or proto), gRPC, and gRPC-Web over the same
:6380 h2c socket — no sidecar, no gateway.
When to use it (and when not)
Good fit
- Real-time recommenders — user → item interactions as decaying edges;
illuminate(user, step=2, k=10, weighting=tfidf)returns a candidate set that already discounts popular items. - Session-aware personalization — a short-TTL session graph layered on a long-TTL preference graph in the same store.
- Fraud / abuse co-occurrence — accounts, devices, IPs as vertices; suspicious co-occurrences as additive edges that self-clean as they decay.
- Trend detection — query → result edges tick up on each interaction and fall off when the trend dies.
- Online graph features for ML — neighborhood aggregations served at request time instead of from a batch feature store.
- Short-lived shared context for agents / sessions — entity-relation
state scoped to a session TTL, queried with
illuminate.
Not a good fit
- Durability out of the box. Lantern is in-memory: a restart loses the graph (a periodic snapshot backup with restore-on-boot is built in, but there is no WAL). Replay your event stream on boot, or put a queue in front.
- Whole-graph analytics — global PageRank, community detection across
billions of edges. (Seed-local PPR and community via
illuminateare supported online queries.) - Working sets beyond one process's RAM. Built-in leaderless replication gives you HA — every replica holds the full graph — but there is no sharding.
- Strong-consistency multi-writer. The store is a leaderless full-replica cache with last-writer-wins per key under an HLC clock, not a linearizable database.
Architecture at a glance
flowchart LR
subgraph Clients
Admin["lantern-admin (admin/)<br/>browser console"]
GoSDK["sdks/go"]
NodeSDK["sdks/node"]
CLI["lantern-cli"]
MCP["lantern-mcp (mcp/)"]
Other3P["any Connect / gRPC /<br/>gRPC-Web client"]
end
Admin -->|Connect-Web| SVC
GoSDK -->|Connect / gRPC| SVC
NodeSDK -->|Connect / gRPC| SVC
CLI -->|Connect / gRPC| SVC
MCP -->|via sdks/go| SVC
Other3P -->|":6380 (one h2c socket)"| SVC
subgraph Server["lantern-server — one of N full replicas"]
direction TB
SVC["LanternService"]
VC["vertex cache (TTL)"]
EC["edge cache (additive + TTL)"]
IX["search index (BM25)"]
W["GC loop"]
Repl["replication pump<br/>(HLC + mutation log)"]
SVC --> VC
SVC --> EC
SVC --> IX
W -.compacts.-> VC
W -.compacts.-> EC
SVC <--> Repl
end
Peers[("peer replicas<br/>(HA mode)")]
Repl <-->|Subscribe / Snapshot| Peers
- One wire surface. The
:6380listener accepts Connect, gRPC, and gRPC-Web on the same h2c socket; every client in this repo — the Admin SPA, both SDKs, the CLI, the MCP server — shares the exact contract from proto/graph/v1/. - HA is optional and leaderless. Every replica holds the full graph;
writes commit locally and fan out asynchronously via
Subscribe/Snapshotstreams tagged with HLC timestamps. No leader, no quorum, no external storage. External CDC consumers attachSubscribeto any one replica and observe every cluster mutation. RFC: docs/replication.md; operator playbook: docs/ha-runbook.md. - lantern-admin (admin/) — a browser-only React Router / Sigma.js console that talks Connect-Web straight to the server: graph visualization, data browsing, search, and a web CLI.
- lantern-mcp (mcp/) — optional Model Context Protocol server that exposes a Lantern instance as shared working context for agent fleets (presence, advisory claims, activity heat, a blackboard — all built on decaying state, where expiry is exactly the semantics you want). See mcp/README.md.
The RPC surface
Defined in proto/graph/v1/graph.proto, served by server/service/service.go. Every read, write, and delete has singular and plural forms; the plural is the canonical implementation, the singular a thin one-element facade — pick whichever reads better at the call site.
| RPC | Purpose |
|---|---|
GetVertex / GetVertices |
Fetch by key; plural reports gaps in Missing instead of erroring |
PutVertex / PutVertices |
Upsert with TTL; last write wins, or conditional insert with if_absent (SET NX) |
DeleteVertex / DeleteVertices |
Remove vertices; incident edges reaped on the next GC tick |
GetEdge / GetEdges |
Current live weight — the sum of unexpired contributions |
AddEdge / AddEdges |
Append weighted contributions (the additive model above); returns the post-accumulation live weight |
PutEdge / PutEdges |
Idempotent replace under one write lock |
DeleteEdge / DeleteEdges |
Remove edges outright |
ScanVertices / ScanVertexKeys / ScanEdges |
Cursor-paginated prefix enumeration, ascending or descending via order (keys-only variant is wire-efficient; edge scans filter on tail and/or head prefix) |
CountVerticesByPrefix / DeleteVerticesByPrefix / DeleteEdgesByPrefix |
Namespace count / capped bulk delete with dry_run (the edge variant removes the tail∩head intersection) |
TopVerticesByDegree |
Rank the most-connected live vertices under a key prefix (out / in / both, optional weighted) — a read-only, point-in-time aggregate |
SearchVertices |
BM25-ranked full-text over vertex content, with match-mode / phrase / fuzzy / prefix-term options |
Illuminate |
Walk the graph from a seed — the shaped-subgraph query described above |
SDK batch writes auto-chunk; a validation interceptor rejects oversize keys
and batches (LANTERN_MAX_KEY_LEN, LANTERN_MAX_BATCH_SIZE) and NaN/Inf
weights before they touch the cache.
The CLI
One grammar, three surfaces: the interactive REPL (lantern-cli repl),
verb-first shell one-liners, and the admin web /cli — they never diverge.
get vertex <key>
put vertex <key> <value> [ttl_seconds] [type=auto|string|int|float|bool|datetime|duration|json]
delete vertex <key> [<key> …]
get edge <tail> <head>
add edge <tail> <head> <weight> [ttl_seconds]
put edge <tail> <head> <weight> [ttl_seconds]
delete edge <tail> <head> [<tail> <head> …]
scan vertices <prefix> [limit] [all=true]
scan edges <tail-prefix> [limit] [head=<prefix>] [all=true]
count vertices <prefix>
delete-prefix vertices <prefix> [limit=<int>] [confirm=yes|dry_run=true]
keys <prefix> [limit]
illuminate <seed> <step> <k> [algorithm=none|mst|spt|ppr|community] [objective=min|max] \
[weighting=raw|tfidf|bm25] [prefix=<string>] \
[restart_prob=<float>] [epsilon=<float>]
help
exit
# One-liners: same grammar, prefixed with the binary.
lantern-cli put vertex alice '{"name":"Alice"}' type=json
lantern-cli delete vertex alice bob carol
lantern-cli scan vertices users/ all=true > snap.json
lantern-cli delete-prefix vertices tmp/ dry_run=true
# Outside the grammar: search, streamed bulk load, and whole-graph backup.
lantern-cli search "desk lamp" --mode all --limit 20
cat edges.ndjson | lantern-cli bulk edges add -
lantern-cli dump graph.pb && lantern-cli restore graph.pb
# TLS / auth (global flags precede the verb).
lantern-cli --tls --tls-ca ./ca.pem -H lantern.example.com -p 443 get vertex alice
lantern-cli --token "$LANTERN_TOKEN" get vertex alice
Every subcommand has long-form help (lantern-cli <cmd> --help); reads emit
JSON on stdout, writes print OK. Exit codes: 0 success, 1 local/parse
error, 2 RPC error. Values quote C-style with "…" (escapes) or verbatim
with '…'.
Pre-built binaries for Linux, macOS, and Windows (amd64 + arm64) are
attached to every release;
macOS users can brew install --cask lantern-cli.
Deploying
Docker Compose — cluster + Admin UI in one command
The fastest way to a running HA cluster with a browser console in front of
it. One up starts three replicas, the Admin SPA, lantern-mcp, and
Prometheus — no local build:
cd deploy/compose
docker compose up -d --pull always
Then open http://localhost:8080 — the Admin loads ready to
illuminate, browse, search, and run ops against the live cluster. The
replicas pin host ports 6380–6382 (the Admin's Gateway button picks
which one to hit); Prometheus scrapes them all on :9091. Details and load
balancing options: deploy/compose/README.md.
Kubernetes (HA)
The bundled Helm chart deploys a StatefulSet with DNS-based peer
discovery, anti-entropy reconciliation, and a PodDisruptionBudget:
helm install lantern deploy/helm/lantern
Values reference: deploy/helm/lantern/README.md. Operational guidance (signals, partitions, upgrades, recovery): docs/ha-runbook.md.
Single instance
Run one instance with every LANTERN_PEER_* env unset and Lantern is a
plain fast in-memory KVS: the peer pump is a no-op and Subscribe still
works as a CDC stream. Pair it with the built-in snapshot backup
(docs/backup.md) so a restart re-seeds the graph.
Container images
Published to GHCR on every release tag, multi-arch, cosign-signed:
ghcr.io/anaregdesign/lantern
(server), ghcr.io/anaregdesign/lantern-admin, and
ghcr.io/anaregdesign/lantern-mcp.
Configuration
Everything is LANTERN_* env vars. The exhaustive, generated reference is
docs/env.md — the ones you'll reach for first:
| Variable | Default | Meaning |
|---|---|---|
LANTERN_PORT |
6380 |
RPC listen port (Connect / gRPC / gRPC-Web multiplexed) |
LANTERN_GC_INTERVAL_SECONDS |
60 |
Cache GC tick |
LANTERN_MAX_VERTICES / LANTERN_MAX_EDGES |
0 |
Capacity soft caps; writes fail fast with RESOURCE_EXHAUSTED at the cap (0 = unlimited). Pair with GOMEMLIMIT. |
LANTERN_AUTH_TOKENS |
(unset) | Comma-separated bearer tokens arming data-plane auth; multiple entries allow zero-downtime rotation |
LANTERN_TLS_CERT_FILE / LANTERN_TLS_KEY_FILE / LANTERN_TLS_CLIENT_CA_FILE |
(unset) | TLS; the client CA enables mTLS |
LANTERN_CORS_ALLOWED_ORIGINS |
(empty) | CORS allow-list for browser clients (the Admin needs its origin here) |
LANTERN_BACKUP_* |
off | Periodic whole-graph snapshot + restore-on-boot |
LANTERN_RATE_LIMIT_RPS |
0 |
Global token-bucket rate limit |
LANTERN_SCAN_DEFAULT_LIMIT / LANTERN_SCAN_MAX_LIMIT |
1000 / 10000 |
Page-size default and hard cap for the Scan* RPCs |
LANTERN_ILLUMINATE_MAX_STEP / LANTERN_ILLUMINATE_MAX_K |
16 / 1024 |
Traversal depth / fan-out caps |
LANTERN_METRICS_ADDR |
:9090 |
Prometheus + health HTTP listener |
LANTERN_STRICT_CONFIG |
false |
Turn malformed/unknown LANTERN_* values into boot failures |
One default worth knowing: a write that omits TTL is stored permanently — decay is opt-in per write.
Observability
Production-grade out of the box — details in the source links:
- Prometheus metrics on
LANTERN_METRICS_ADDR/metrics: standardgrpc_server_*RPC metrics (canonical names retained so existing dashboards keep working) plus domain gauges/counters —lantern_vertices,lantern_edges,lantern_ttl_expirations_total,lantern_gc_duration_seconds,lantern_build_info. - Structured logging via
log/slog(JSON by default) with per-RPC start/finish events. - Health checks:
grpc.health.v1.Healthon:6380plus HTTP/healthzand/readyzon the metrics listener — Kubernetes probes andgrpc_health_probeboth work.LANTERN_DRAIN_DELAY_SECONDSgives zero-drop rolling updates. - OpenTelemetry tracing: set
OTEL_EXPORTER_OTLP_ENDPOINTand every request gets a span; without it the tracer stays noop with zero overhead. - gRPC reflection on by default (handy for
grpcurl); disable withLANTERN_REFLECTION=false. - HTTP/2 keepalive tuning and a panic-recovery interceptor that turns
panics into
Internalresponses with a logged stack trace.
Repository layout
A monorepo of six Go modules stitched by go.work plus two Bun-managed TypeScript packages; dependency direction is a strict DAG:
| Path | What it is |
|---|---|
proto/ |
The .proto contract everything shares |
pb/ |
Generated protobuf + Connect-Go stubs (never hand-edited) |
core/ |
Reusable graph / cache / collections / concurrency / NLP building blocks |
server/ |
The Connect server (DI via google/wire) |
sdks/go/ |
Go client SDK — depends on pb/ only |
sdks/node/ |
TypeScript client SDK (lantern-sdk on npm) |
cli/ |
lantern-cli — REPL + one-liners |
admin/ |
Browser Admin console (React Router / Fluent UI / Sigma.js) |
mcp/ |
MCP server exposing Lantern to agent runtimes |
deploy/ |
Docker Compose stack + Helm chart |
docs/ |
Env reference, replication RFC, HA runbook, backup guide |
Limitations (the honest section)
- In-memory first. Snapshot backup + restore-on-boot is built in, but there is no WAL — writes between snapshots are lost on crash. Deployments needing stronger durability replay events from a durable log on boot.
- HA, not sharding. Leaderless full-replica replication is built in; the working set must still fit in one process's RAM.
- Auth is
requirepass-tier. Static bearer tokens and TLS/mTLS — no users, ACLs, or per-namespace authorization. Front it with a mesh or sidecar if you need identity-based access control. - One global
sync.RWMutexon the graph cache (plus per-edge mutexes on weight aggregation). Read-heavy workloads scale well; write-very-hot keys serialize.
Contributing
Issues and PRs welcome. Start with CONTRIBUTING.md for the process contract and AGENTS.md for the full development guide (module rules, codegen, quality gate). The short version:
go build -v ./... # build
go test ./... # test (repeat in each submodule)
go generate ./... # regenerate wire + protobuf stubs — zero install
License
MIT.
Documentation
¶
Overview ¶
Package lantern roots repository-wide `go generate` directives that don't belong to a single module. Buf lives here because the generated protobuf stubs land inside the sdks/go module while the .proto sources sit at the repo root — neither is an obvious home for the codegen, so a workspace-root directive keeps it discoverable.
The wire directive lives in server/generate.go, alongside the module that owns the `tool github.com/google/wire/cmd/wire` declaration.
Run everything with:
go generate ./...
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
Package cmd implements the `lantern-cli` CLI: a cobra-based command tree that exposes every Lantern RPC as a one-shot subcommand and ships the legacy interactive prompt as `lantern-cli repl`.
|
Package cmd implements the `lantern-cli` CLI: a cobra-based command tree that exposes every Lantern RPC as a one-shot subcommand and ships the legacy interactive prompt as `lantern-cli repl`. |
|
core
module
|
|
|
mcp
module
|
|
|
pb
module
|
|
|
sdks
|
|
|
go
module
|
|
|
testbed
|
|
|
bench/release
command
Package main is the release-time bench aggregator.
|
Package main is the release-time bench aggregator. |
|
bench/report
command
Package main is the bench-harness report renderer.
|
Package main is the bench-harness report renderer. |
|
ha-recovery
command
Compose HA failure-recovery test:
|
Compose HA failure-recovery test: |
|
ha-smoke
command
Compose HA smoke test: hammer all 3 replicas via SDK round-robin LB, then verify replication by hitting each per-replica endpoint and confirming vertex/edge state agrees.
|
Compose HA smoke test: hammer all 3 replicas via SDK round-robin LB, then verify replication by hitting each per-replica endpoint and confirming vertex/edge state agrees. |
|
scripts
command
exercise-sdk drives every public method of github.com/anaregdesign/lantern/sdks/go against the running testbed (localhost:6380).
|
exercise-sdk drives every public method of github.com/anaregdesign/lantern/sdks/go against the running testbed (localhost:6380). |