lantern

package module
v0.32.1 Latest Latest
Warning

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

Go to latest
Published: Jul 9, 2026 License: MIT Imports: 0 Imported by: 0

README

Lantern — the in-memory Key-Vertex-Store

lantern

CI Release Go Reference npm

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?"bfs, pagerank, and community walk the live graph and return 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
> bfs 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. The traversal RPC runs 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.

Want a smooth geometric decay instead of a flat TTL cliff? The Go SDK's AddDecayingEdge is a client-side helper (no extra RPC — the server stays a dumb additive store) that expands one DecayOpts{InitialWeight, Ratio, Steps, Interval} into a handful of staggered-TTL contributions and ships them as a single AddEdges batch, so the live weight steps down geometrically (e.g. 16 → 8 → 4 → 2 → 1 → 0) rather than disappearing all at once. The contributions telescope, so they sum to InitialWeight (writing {8,4,2,1,1}, not {16,8,4,2,1}). CLI: add decaying-edge <tail> <head> <initial_weight> <ratio> <steps> <interval_seconds>.

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 RPC walks the live graph from a seed and returns exactly the shape you asked for. The CLI exposes that RPC as three family verbs — bfs, pagerank, and community — with orthogonal knobs for tree rendering, optimization direction, and weighting:

Axis Options What it picks
family verb bfs / pagerank / community Greedy per-hop top-k neighbourhood, Personalized PageRank relevance star, or the seed's natural community (conductance-cut, returned as a real induced subgraph)
reduction none (default) / mst / spt Return the raw family result, or render the bfs / community result as a spanning-tree or shortest-path-tree view rooted at the seed
objective max (default) / min Keep strongest edges vs cheapest edges — the direction of both the bfs per-hop top-k prune and any tree reduction (ignored by pagerank, 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))

bfs a 2 2 reduction=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:

bfs user:42 2 10                                      # raw 2-hop neighbourhood
bfs user:42 3 8 reduction=spt objective=max           # most-relevant path tree
bfs user:42 3 8 reduction=mst objective=min           # clustering / dedup backbone
pagerank user:42 10                                   # PageRank-ranked neighbourhood
community user:42 30                                  # the seed's natural community
community user:42 30 reduction=mst objective=min      # …as a spanning-tree backbone
bfs user:42 2 10 weighting=tfidf                      # suppress hub items

The family verb picks the traversal (bfs, pagerank, community) and the orthogonal reduction axis (none default, mst, spt) renders the bfs/community result as a tree rooted at the seed — so a local community can be handed back as its own minimum-spanning-tree backbone in one RPC. reduction is not accepted by pagerank, which returns a ranked star.

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 bfs, pagerank, or community walk:

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)
> bfs 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)

// One call → a geometric decay curve (client-side fan-out over AddEdges, no new 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))

// 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; bfs user 2 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 bfs, pagerank, or community.

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 are 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 :6380 listener 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 / Snapshot streams tagged with HLC timestamps. No leader, no quorum, no external storage. External CDC consumers attach Subscribe to 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]
bfs        <seed> [step] [fan_out] [reduction=none|mst|spt] [objective=min|max] \
           [weighting=raw|tfidf|bm25] [prefix=<string>]
pagerank   <seed> [top_n] [restart_prob=<float>] [epsilon=<float>] \
           [weighting=raw|tfidf|bm25] [prefix=<string>]
community  <seed> [max_size] [restart_prob=<float>] [epsilon=<float>] \
           [reduction=none|mst|spt] [objective=min|max] \
           [weighting=raw|tfidf|bm25] [prefix=<string>]
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 explore, browse, search, and run ops against the live cluster. The replicas pin host ports 63806382 (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: standard grpc_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.Health on :6380 plus HTTP /healthz and /readyz on the metrics listener — Kubernetes probes and grpc_health_probe both work. LANTERN_DRAIN_DELAY_SECONDS gives zero-drop rolling updates.
  • OpenTelemetry tracing: set OTEL_EXPORTER_OTLP_ENDPOINT and every request gets a span; without it the tracer stays noop with zero overhead.
  • gRPC reflection on by default (handy for grpcurl); disable with LANTERN_REFLECTION=false.
  • HTTP/2 keepalive tuning and a panic-recovery interceptor that turns panics into Internal responses 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.RWMutex on 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
cli
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).

Jump to

Keyboard shortcuts

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