The Stack
Lantern is a polyglot monorepo on purpose. Four languages, three data stores, one contract format. This page is the honest answer to “why is there Rust and Go in here?” — what each piece is, what job it holds, and why that job went to that tool instead of the obvious one.
The rule
Right tool per layer. Never unify for unification's sake. Adding a new language requires an ADR (ADR 0001) — the bar is deliberately high, because every extra language is a second CI matrix, a second set of security advisories, and a second thing a new hire has to learn.
Languages, and the job each one holds
| Layer | Language | Why this one |
|---|---|---|
| Control plane, workflow engine, scheduler, memory, billing | Go 1.23 | Kubernetes is written in Go, so every cluster library is first-class. Compiles to one static binary with no runtime to install. Mature gRPC + Postgres ecosystem. Its concurrency model (goroutines) suits work that is mostly waiting — on a database, on another service. |
| Gateway, model router, runtime manager, surface gateway | Rust 2024 | The hot path. Rust has no garbage collector, so there are no unpredictable pauses while a token stream is in flight — latency stays flat instead of spiking. It also frees memory deterministically, which matters for a process babysitting hundreds of sandboxes. Firecracker itself is Rust, so we speak its native language. |
| Dashboard, landing page, docs site | TypeScript / Next.js 15 | React Server Components + streaming means the run waterfall can render tokens as they arrive rather than after. And it is the same language as our primary SDK, so types flow straight through. |
| Primary SDK | TypeScript | Where the agent ecosystem already lives. |
| Secondary SDKs | Python 3.11+, Go | Python for AI/ML users, Go for infra users. |
CLI (lantern) | Go / Cobra | Static binary, trivial cross-compilation to every OS, and it reuses the same gRPC client the services use. |
| API contracts | protobuf3 | One source of truth for types that cross a service boundary. Generates Go and TypeScript, so a field rename cannot silently drift between two services. |
What these things actually are
If a name below is unfamiliar, this is the whole idea in a sentence — no prior infrastructure background assumed.
| Thing | In one sentence |
|---|---|
| gRPC | How our services call each other — like a web request, but with a schema both sides agreed on in advance, so a typo is a compile error rather than a 3am page. |
| protobuf | The schema language for those calls. You write the message shape once; code for every language is generated from it. |
| Firecracker | A very small, very fast virtual machine built by AWS to run Lambda. Boots in about a tenth of a second and gives untrusted code its own kernel, so escaping it means breaking the hardware boundary, not just a container. |
| gVisor | A sandbox that pretends to be the operating system. The workload thinks it is talking to Linux; it is actually talking to a user-space impersonation that only forwards the safe calls. |
| Kata Containers | Containers that are secretly full virtual machines. Broader compatibility than Firecracker, slower to start — used for hostile input. |
| The harness | Our Rust program that boots as the very first process inside every microVM. It hands out secrets, enforces the egress allowlist, and streams logs and heartbeats home. It is the last trust boundary around the workload. |
| Control plane / data plane | The split between the part that decides (our SaaS: who owns what, what should run) and the part that executes (your cloud account: the actual agent code and your data). Your data never has to leave your infrastructure. |
| pgvector | A Postgres extension that stores embeddings and finds similar ones — semantic search without running a separate vector database. |
| OTel (OpenTelemetry) | The vendor-neutral standard for traces and metrics. Every Lantern service emits it, so one run can be followed across five services in whatever monitoring tool you already own. |
| RLS (Row-Level Security) | A Postgres feature that filters rows by tenant inside the database itself. Tenant isolation survives an application bug, because the database refuses to return the other tenant's rows regardless of what the query asked for. |
Data stores
Three, and adding a fourth requires an ADR. Between them they cover every current need.
| Store | Holds | Why not something else |
|---|---|---|
| Postgres (+ pgvector) | Everything durable: tenants, agents, runs, the event journal, budgets, receipts, embeddings. | Transactions, JSONB, and row-level security in one engine. pgvector means we do not need a separate vector database. |
| Redis | Caching, rate limiting, queues, and the pub/sub that pushes live run events to the dashboard. | This data is allowed to be lost on restart. Putting it in Postgres would mean paying durability costs for something disposable. |
| S3 / MinIO | Large blobs: agent bundles, microVM snapshots, attachments. | Big binary objects do not belong in a relational database. MinIO is the S3-compatible local stand-in for development. |
End to end: one run, all the way through
This is what actually happens between lantern run agent.yaml and a result. Every hop below is a real process boundary.
you ──▶ CLI / SDK / dashboard (Go · TypeScript)
│ REST or gRPC, bearer token
▼
1 CONTROL PLANE (Go, :8080 / :50051)
│ authenticate, resolve tenant, check budget + quota,
│ write the run row, mint a per-instance identity
▼
2 WORKFLOW ENGINE (Go, :50052)
│ the only thing allowed to mutate run state.
│ each step is journaled, so a crash resumes instead of restarting
├──────────────▶ 3 MODEL ROUTER (Rust, :50053)
│ picks a real vendor model from a capability
│ name like "reasoning-large", handles failover,
│ meters tokens and cost
▼
4 RUNTIME SCHEDULER (Go, :50055)
│ picks a node: warm pool, region, fair share, cost, health
▼
5 RUNTIME MANAGER (Rust, :50054)
│ spawns the workload in the isolation class the spec declared
│ (Firecracker · Kata · K8s Job · Wasmtime · Docker)
▼
6 HARNESS — PID 1 inside the sandbox (Rust)
│ vends short-lived secrets, enforces the egress allowlist,
│ streams logs, heartbeats, and cost back up
▼
your agent code runs
results flow back the same way, streaming:
harness ──▶ manager ──▶ control plane ──▶ SSE ──▶ dashboard / SDK
│
└──▶ journal_events (Postgres)
the durable record: replay, receipts, auditThe rules that hold it together
The stack choices above only pay off because a handful of boundaries are never crossed. These are load-bearing — breaking one causes an incident, not a bug.
- The control plane never touches your code. Only the runtime manager talks to Firecracker or Kata or pods.
- One writer for run state. Services emit events; the workflow engine is the only thing that writes the outcome. No service updates the
runstable directly. - Anything slow is durable. If it can take more than 100ms or calls a model, it becomes a journaled step — idempotent and replayable, so a restart resumes mid-run.
- Streaming never buffers. No service is allowed to collect a whole response and then forward it.
- Untrusted code gets a microVM. Never a bare container. If the cluster cannot provide the declared isolation, the workload is refused rather than quietly downgraded.
- Models are named by capability, not vendor. Your code says
reasoning-large; the router decides it is a specific vendor model today and something better next month. - Multi-tenant by default. Every row carries a tenant, every call carries a tenant, and Postgres row-level security enforces it below the application.
- Secrets never appear in logs, traces, or run state. Only a reference travels; the harness resolves it at the moment of use.
What this costs us
Being honest about the bill, because a stack page that only lists benefits is marketing:
- Four language toolchains in CI. Every pull request runs golangci-lint,
cargo clippy -D warnings, and eslint/tsc. Slower pipeline, more supply-chain surface to audit. - Contract changes are two-step. Touching a proto means regenerating, then fixing both the Go and the Rust side. Deliberate — the friction is what stops silent drift.
- A wider hiring surface. Nobody is deep in all four. The mitigation is that the boundaries are narrow enough to work in one layer without holding the others in your head.
- Rust is slower to write. We accept that only where latency or memory determinism actually pays for it — never for CRUD.