lantern

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 0 Imported by: 0

README

Lantern — an in-memory graph-based key-vertex store

lantern

Lantern is a time-decaying, in-memory graph KVS that speaks gRPC. It behaves like a key-value store — values are vertices — and lets you walk the relationships between them in real time. Both vertices and edges carry their own TTLs, so the graph naturally forgets old information the same way real-world relationships fade.

It is not an ontology engine, not a global-shortest-path solver, and not a disk-backed graph database. It is the small, hot, online piece you put in front of those systems so request-path code can ask "who is this user related to right now, and how strongly?" in a single millisecond-scale RPC.


Why Lantern is different

Most graph stores are optimized for offline analytics on a snapshot of "what the graph looked like yesterday." Lantern is built around three properties that together make it well suited to online, behavioral workloads:

1. Time-decaying graph (per-edge & per-vertex TTL)

Every vertex and every edge carries an independent expiration. A background janitor (Watch) compacts expired entries and drops edges whose endpoints have disappeared. There is no manual deletion required to keep the working set warm and small.

2. Additive edge weights with independent expiration

Edges are not single scalars — each AddEdge(tail, head, w, ttl) call appends another contribution with its own TTL. The reported weight is the live sum of contributions that have not yet expired.

t=0   AddEdge(a, b, 1.0, 3s)   →  weight(a,b) = 1
t=1   AddEdge(a, b, 1.0, 3s)   →  weight(a,b) = 2   (two contributions live)
t=3   first contribution expires →  weight(a,b) = 1
t=4   second contribution expires →  weight(a,b) = 0   (edge gc'd)

This is the model you actually want for behavioral signals: every click, view, or co-occurrence event simply gets appended; "how strong is this relationship right now" falls out of the math, no batch job required. Use PutEdge when you want classic idempotent replace instead.

3. Built-in online graph algorithms over the live snapshot

A single Illuminate RPC walks the live graph from a seed vertex and returns a subgraph already shaped for your use case:

Optimization What you get Typical use
(none) k-nearest neighbors per hop, up to N hops "what is related to X"
MINIMUM_SPANNING_TREE MST rooted at seed clustering / dedup
MAXIMUM_SPANNING_TREE Max-spanning tree strongest-relationship backbone
SHORTEST_PATH_TREE SPT using raw weights as cost low-cost reachability
SHORTEST_PATH_TREE_INVERSE SPT using 1/weight as cost most-relevant path tree
tfidf=true flag Per-hop top-k weighted by w / log2(1+df(head)) suppress hub vertices like "popular" items

You don't fetch a wall of edges and post-process — the server returns exactly the shape you asked for.


When to use it (and when not)

Good fit

  • Real-time recommenders — user → item interaction graph with decaying weights; illuminate(user, step=2, k=10, tfidf=true) gives a candidate set that already discounts popular items.
  • Session-aware personalization — short-TTL session graph layered on top of long-TTL preference graph in the same store.
  • Fraud / abuse co-occurrence signals — accounts, devices, IPs as vertices; suspicious co-occurrences as additive edges that decay so old noise self-cleans.
  • Trend & "what's hot" detection — edges from query → result tick up on each interaction and naturally fall off when the trend dies.
  • Short-term knowledge graph for LLM / agent context — keep entity-relation cache scoped to a session TTL; query with Illuminate to build prompt context.
  • Online graph features for ML — neighborhood aggregations served at request time instead of from a feature store batch.

Not a good fit

  • Anything that needs durability out of the box. Lantern is in-memory only — a restart loses the graph. Replay your event stream into it on boot, or put a queue in front.
  • Global graph analytics (PageRank over the whole graph, community detection across billions of edges, etc.).
  • Massive working sets that don't fit in one process's RAM. There is no built-in sharding or replication today.
  • Strong-consistency multi-writer scenarios. The store is a single-process cache with internal locking, not a distributed database.

Architecture at a glance

flowchart LR
    subgraph Client
        SDK["Go client SDK<br/>(client/)"]
        CLI["lantern-cli<br/>(cli/)"]
        Other["any gRPC client<br/>(proto/graph/v1)"]
    end
    subgraph Server["lantern-server (server/)"]
        SVC["LanternService<br/>(gRPC)"]
        GC["GraphCache[string, *Vertex]"]
        VC["vertex cache<br/>(TTL)"]
        EC["edge cache<br/>(additive + TTL)"]
        W["Watch loop<br/>(GC every 1m)"]
    end
    SDK & CLI & Other -->|gRPC :6380| SVC
    SVC --> GC
    GC --> VC
    GC --> EC
    W -.compacts.-> VC
    W -.compacts.-> EC
  • DI: google/wire — see server/cmd/wire.go. Never edit wire_gen.go by hand; run go generate ./... after changing providers.
  • Generic GraphCache[S, T] is instantiated as GraphCache[string, *Vertex] at the wire boundary (wire cannot synthesize generic type arguments).

Quick start

Run the server
docker run --rm -p 6380:6380 ghcr.io/anaregdesign/lantern:latest

Or build from source:

go run ./server/cmd          # listens on :6380
Use the CLI
go build -o lantern ./cli
./lantern --help

The CLI exposes every RPC as a scriptable subcommand with verbose, LLM-friendly help text (lantern <cmd> --help). All read commands emit JSON on stdout; all write commands print a single OK line.

# writes
./lantern vertex put alice '{"name":"Alice"}' --value-type json --ttl 1h
./lantern edge   add alice bob 1.5            # additive (sums weight)
./lantern edge   put alice bob 0.7            # idempotent (replaces)

# reads
./lantern vertex get alice                    # JSON: {key,value,expiration}
./lantern edge   get alice bob                # JSON: {tail,head,weight,expiration}
./lantern illuminate alice --step 2 --k 5 --optimize mst

# batches & streaming
./lantern vertex delete alice bob carol       # batch DeleteVertices
cat edges.ndjson | ./lantern bulk edges add - # streamed AddEdges

# TLS / mTLS
./lantern --tls --tls-ca ./ca.pem -H lantern.example.com -p 443 vertex get alice

# legacy interactive prompt
./lantern repl

Global flags include --host/--port (or --address), --timeout, --tls*, --compression {none|gzip}, and --chunk-size. Exit code 0 is success, 1 is a local / parse error, 2 is a gRPC error from the server.

Use it from Go
import "github.com/anaregdesign/lantern/client"

cli, err := client.NewLantern("localhost:6380")
if err != nil { log.Fatal(err) }
defer cli.Close()

ctx := context.Background()

// Vertices accept string, int (signed/unsigned), float, bool, time.Time,
// time.Duration, []byte, or 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.
_ = 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", 2, 3, true)

The full multi-type, additive-edge, and Illuminate example lives in client/example/main.go.

Use it from another language

Generate bindings from proto/graph/v1/graph.proto with your favorite protoc plugin or buf. The service is plain unary gRPC, no streaming required.


gRPC surface

Defined in proto/graph/v1/graph.proto, served by server/service/service.go.

RPC Purpose Notes
PutVertex Upsert a vertex with TTL Last write wins
GetVertex Fetch a vertex by key NotFound if expired/missing
DeleteVertex Remove a vertex Edges to/from it are GC'd on next Watch tick
DeleteVertices Batch remove vertices in one round-trip SDK auto-chunks at WithBatchChunkSize; idempotent (retried automatically)
AddEdge Append a weighted contribution Not idempotent — see §1.2 above
PutEdge Idempotent replace (delete + add) Use when you want one-and-only-one weight
GetEdge Read current live weight Sum of unexpired contributions
DeleteEdge Remove an edge outright
DeleteEdges Batch remove edges in one round-trip Takes []EdgeRef{Tail, Head}; SDK auto-chunks; idempotent
Illuminate Walk the graph from a seed step, k, tfidf, and Optimization (none / MST / max-ST / SPT / inverse-SPT)

Vertices auto-materialize on AddEdge/PutEdge if the endpoint key does not yet exist (they get the edge's expiration as their TTL). This keeps event-stream ingestion simple — you only need to issue edge writes.


Configuration

The server is configured via environment variables, parsed in server/provider/provider.go:

Variable Default Meaning
LANTERN_PORT 6380 gRPC listen port
LANTERN_DEFAULT_TTL_SECONDS 60 Default TTL when a request omits one
LANTERN_GC_INTERVAL_SECONDS 60 Cache GC tick interval
LANTERN_LOG_LEVEL info debug / info / warn / error
LANTERN_LOG_FORMAT json json or text (slog handler)
LANTERN_METRICS_ADDR :9090 Address for Prometheus + health HTTP; empty disables
LANTERN_REFLECTION true Register gRPC server reflection (useful for grpcurl)
LANTERN_SHUTDOWN_TIMEOUT_SECONDS 30 Upper bound on graceful shutdown before forcing Stop()
LANTERN_MAX_RECV_MSG_BYTES 16777216 Per-RPC inbound message limit (16 MiB default)
LANTERN_MAX_SEND_MSG_BYTES 16777216 Per-RPC outbound message limit
LANTERN_MAX_CONCURRENT_STREAMS 1024 Upper bound on concurrent streams per HTTP/2 connection
LANTERN_RATE_LIMIT_RPS 0 Global token-bucket rate limit; 0 disables
LANTERN_RATE_LIMIT_BURST max(1, rps) Burst capacity for the rate limiter
LANTERN_MAX_KEY_LEN 1024 Reject vertex/edge keys longer than this (validation interceptor)
LANTERN_MAX_BATCH_SIZE 10000 Reject batch Put/Add requests over this size
LANTERN_ILLUMINATE_MAX_STEP 16 Cap on BFS depth accepted by Illuminate
LANTERN_ILLUMINATE_MAX_K 1024 Cap on neighbours-per-step accepted by Illuminate
LANTERN_TLS_CERT_FILE (unset) Server certificate; enables TLS when set with key
LANTERN_TLS_KEY_FILE (unset) Server private key
LANTERN_TLS_CLIENT_CA_FILE (unset) Client CA bundle; enables mTLS (RequireAndVerifyClientCert) when set

Observability

Lantern ships production-grade observability out of the box:

  • Structured logging via log/slog — JSON by default, with per-RPC start/finish events emitted by the grpc-ecosystem/go-grpc-middleware/v2 logging interceptor.
  • Prometheus metrics — gRPC server metrics (including a handling-time histogram) plus Go runtime and process collectors, exposed on LANTERN_METRICS_ADDR at /metrics.
  • Health checks — gRPC standard health service (grpc.health.v1.Health) and HTTP /healthz + /readyz on the metrics listener, so both Kubernetes probes and grpc_health_probe work.
  • Distributed tracing — OpenTelemetry server instrumentation via otelgrpc.NewServerHandler() (modern stats-handler API; the deprecated unary/stream interceptors are not used). Wire up an exporter via the standard OTEL_EXPORTER_OTLP_* env vars to ship traces.
  • gRPC reflection — registered by default; turn off with LANTERN_REFLECTION=false for hardened deployments.
  • Keepalive + panic recovery — sensible keepalive.ServerParameters and an enforcement policy are applied, and a recovery interceptor turns panics into Internal status responses with a logged stack trace instead of crashing the process.

Repository layout

This is a monorepo consolidating four formerly separate repositories.

Path Role
server/ gRPC server (DI via google/wire)
client/ Go client SDK
cli/ Interactive CLI (cobra + promptui) — formerly lantern-cli
proto/ .proto sources — formerly lantern-proto
gen/go/ Generated Go bindings (regenerate with go generate ./...)
core/ Shared building blocks reused by server & client: graph algorithms, TTL caches, collections, concurrency, NLP

Developing on Lantern

make build       # go build -v ./...
make test        # go test -v ./...
make test-race   # go test -race -shuffle=on -covermode=atomic ./...
make fmt         # gofmt -s -w .
make vet         # go vet ./...
make generate    # go generate ./...  (runs wire + buf — no install required)
make wire        # alias: go tool wire ./server/cmd
make proto       # alias: buf generate --clean (uses system `buf` if present, else `go run`)
make vuln        # govulncheck ./...
make tidy        # go mod tidy

Codegen is one command:

go generate ./...

This regenerates both server/cmd/wire_gen.go and everything under gen/go/. No CLIs need to be installed up front:

  • wire is wired in via the tool directive in go.modgo tool wire just works after go mod download.
  • buf is invoked via go run github.com/bufbuild/buf/cmd/buf@v1.70.0 when no system buf is on PATH. Installing buf locally only makes the first invocation faster; correctness is identical.

Required toolchain:

  • Go 1.26 — kept in lockstep across go.mod, the Dockerfile (golang:1.26-alpine), and .github/workflows/go.yml. Bumping the version means bumping all three.
Conventions and gotchas
  • Never hand-edit server/cmd/wire_gen.go — it is generated. Edit providers in server/provider/provider.go or definitions in server/cmd/wire.go, then re-run go generate ./... (or make wire for just the wire step).
  • wire + generics — wire cannot synthesize generic type arguments, so the provider returns the concrete *graph.GraphCache[string, *Vertex]. Re-check this constraint before introducing generics there.
  • Adding a new vertex value type in the Go SDK requires updating both directions in client/value.go: nativeVertex.asVertex() (Go → proto) and the matching *Value() accessor (proto → Go).
  • Proto go_package is github.com/anaregdesign/lantern/gen/go/graph/v1. make proto rewrites everything under gen/go.
  • Not every *Response message has a Status field — check gen/go/graph/v1/graph.pb.go before patching response types.
  • Test gaps — there are currently no tests for the server/service layer, wire wiring, or client transport paths. Add at least minimal table tests in the same PR for non-trivial changes there.
CI / release

CLI cheatsheet

put vertex <key:string> <value:string> [<ttl:int>]
put edge   <tail:string> <head:string> <weight:float> [<ttl:int>]
get vertex <key:string>
get edge   <tail:string> <head:string>
illuminate <neighbor|spt_cost|spt_relevance|mst_cost|mst_relevance> \
           <seed:string> <step:int> <k:int> <tfidf:bool>

Worked examples (with diagrams) live in docs/cli-examples.md below.

CLI walkthrough
Put a couple of vertices and an edge

Asset 5

> put vertex a A
OK (454.695µs)
> put vertex b B
OK (768.012µs)
> put edge a b 1
OK (642.748µs)
> get vertex a
{"String_":"A"}
> get edge a b
1.000000
Explore neighborhoods with illuminate

After loading a richer graph:

Asset 6

> illuminate neighbor a 1 2 false
{
  "vertices": { ... },
  "edges": { "a": { "b": 1, "c": 1 } }
}

Asset 9

Shortest-path tree by cost vs. relevance
> illuminate spt_cost a 2 2 false        # uses raw weight as cost
> illuminate spt_relevance a 2 2 false   # uses 1/weight as cost

Asset 7 Asset 8


Roadmap & limitations

  • Single-process, in-memory only. Multi-node sharding and replication are not built in. Production deployments typically replay events from a durable log (Kafka, etc.) into Lantern on boot.
  • No authn / authz built in. TLS and mTLS are supported out of the box via the LANTERN_TLS_* env vars (and the matching --tls* client flags), but per-RPC authentication is not — front it with a service mesh or envoy-style sidecar if you need identity-based access control.
  • Single global sync.RWMutex on the graph cache plus per-edge mutexes on weight aggregation. Read-heavy workloads scale well; write-very-hot keys serialize.

PRs and issue discussion welcome.


License

See LICENSE.

Documentation

Overview

Package lantern roots a few repository-wide `go generate` directives so that `go generate ./...` regenerates everything codegen-related without anyone having to install extra CLIs first.

  • wire is pulled in via the `tool` directive in go.mod, so `go tool wire` just works after `go mod download`.
  • buf is invoked through `go run` against a pinned version so contributors don't need a system-wide `buf` binary either. CI uses bufbuild/buf-setup-action which is faster, but the `go run` fallback keeps local dev one-command.

Run everything with:

go generate ./...

Directories

Path Synopsis
cli
cmd
Package cmd implements the `lantern` CLI: a cobra-based command tree that exposes every Lantern gRPC RPC as a one-shot subcommand and ships the legacy interactive prompt as `lantern repl`.
Package cmd implements the `lantern` CLI: a cobra-based command tree that exposes every Lantern gRPC RPC as a one-shot subcommand and ships the legacy interactive prompt as `lantern repl`.
example command
core module
cache/example command
graph/example command
nlp
gen
go/graph/v1
Package graphv1 is a reverse proxy.
Package graphv1 is a reverse proxy.
mcp module
pb module
sdks
go module
server
cmd command

Jump to

Keyboard shortcuts

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