React vs Vantage

Periscope (Headlamp clone)

Kubernetes (k8s) is the engine behind much of today's infrastructure — it runs the containerised apps that power a huge share of the modern web. Headlamp is a popular window into it — a dashboard for seeing what's running across a cluster (one Kubernetes setup). It's open-source, written in React, backed by the CNCF, and built over six years by hundreds of contributors.

A real test of Vantage is whether it can rebuild the kind of app our audience already relies on. So we set one: clone the core of Headlamp — the part that browses a cluster — with Vantage and three prompts to Claude Opus 4.8.

The result surprised us. Vantage passed — and the finished app is pure YAML with a few Rhai formulas, about 800 lines. It needed one new piece first: a driver teaching Vantage to speak Kubernetes. We wrote that once and shipped it in Vantage 0.20, so building Periscope today takes just those 800 lines. Here is the difference, graph by graph.

01 Lines of code you write

React · Headlamp~52,000 lines
  • Resource views~12,600
  • Vantage UI~18,000
  • K8s client~14,000
  • Vantage Framework~8,000
Vantage · Periscope~800 lines
  • YAML + Rhai · the app~800
  • K8s clientbuilt-in · 0.20
  • Vantage UIincluded
  • FrameworkVantage (OSS)

02 The same window onto a cluster — drag to compare

Headlamp — the Workloads list in React Headlamp · React
Periscope — the cluster view in Vantage Periscope · Vantage

03 What you get, side by side

HeadlampReact + Go PeriscopeVantage
Browse the cluster
Resources as related, drillable tables
Live metric dashboards
Summary & detail panels
Grouped sidebar & theming
Beyond browsing — Headlamp's advanced features
Edit & apply resources
Plugins & extensions
Multi-cluster
RBAC, port-forward, exec
Helm, topology map, search
Cost to build & support
Lines of code you write~52,000~800
Time to build~6 yearsan afternoon
People to build & support~330 contributors1 AI agent

The ~52,000-line figure is Headlamp with its advanced features stripped out — the browse-and-view core only, the part Periscope reproduces. We are not rebuilding the rest (editing, plugins, multi-cluster, RBAC…) in Vantage; those are the next handful of prompts, not the next six years.

Prompt one

First, teach Vantage to read your cluster

Before we can build the Periscope app, Vantage UI has to be able to talk to your Kubernetes cluster directly — to query it live, in real time, the way Headlamp does. At the time of this experiment Vantage was at release 0.19, and it couldn't: it already connected to SQL databases, REST APIs and document stores, but Kubernetes was new to it.

Teaching it a brand-new backend is a documented path, written for an AI agent to follow. We pointed Claude Opus 4.8 at it and watched it work. Read the steps below for the plain-language story; expand any one to see how it was built.

smart_toyPrompt 1 · Claude Opus 4.8

Using Vantage docs and "vantage" open-source repository — implement a new DataSource that speaks the Kubernetes API. Stop when all steps are fully completed and tested.

Completed in ~15 minutes, cost under $5.00

psychologyVantage's docs contain a special guide for creating a new DataSource (or persistence) in nine concrete steps that I'll follow closely. Once the work is done and built into Vantage UI, it will speak the Kubernetes API.

1Step 1 · Type system Step 1 is about lining up the data types. A capable database like PostgreSQL has dozens of them — lists, geographic points, and more. The Kubernetes API needs far fewer, with two all its own: a Quantity (an amount of CPU or memory) and a Time. I implement every type it supports — with tests — exactly as the guide lays out. expand_moreHow it's builtHide detail
First, agree on what a value is

Every backend has its own type system, and they vary wildly in size. A CSV gets by with one — everything is text. Vantage's SQLite type system has seven variants, Postgres fourteen, SurrealDB twenty-four. Vantage is strict about type boundaries and safety, so this first step defines that type system: the variants, the conversions to and from native Rust types, and clean round-tripping through CBOR / Serde. You declare it with a single macro:

vantage_type_system! {
    type_trait: KubeType,
    value_type: serde_json::Value,
    type_variants: [Null, Bool, Integer, Number, Quantity, Text, Time],
}

Kubernetes brings its own oddities — a Quantity and a Time — and the Vantage type system takes them in stride. The agent reads the guide and builds the right variants, so "16331752Ki" becomes a real number you can sort and chart, and "250m" a quarter of a CPU — not text.

Read furtherarrow_forward

2Step 2 · Expressions · skipped A powerful database server can run expressions itself — doing the work server-side and sending back only the data asked for. Vantage takes advantage of that whenever it can. So this step asks whether the Kubernetes API can run arbitrary expressions and server-side logic. The answer is no — so the implementation is skipped. expand_moreSee what's skippedHide detail
Expressions — the backend's own query language

Many backends ship a query engine — SQL, SurrealQL, GraphQL. Vantage supplies the mechanics for type-safe expression building: with expressions, many operations run server-side, and Vantage takes full advantage of that. The same primitives exist in both Rust and Rhai — a CASE, for instance, reads the same either way:

Rust
let kind = Case::new()
    .when(ident("status").eq("active"), "yes")
    .when(ident("status").eq("banned"), "no")
    .else_("unknown");
Rhai
case_when()
    .when(row["status"] == "active", expr("'yes'"))
    .when(row["status"] == "banned", expr("'no'"))
    .else_(expr("'unknown'"))
    .expr()

Not needed for Kubernetes — the API has no query language to build against, so the agent skips this step and filters the fetched list instead.

Read furtherarrow_forward

3Step 3 · Operators & conditions · skipped Operators let a server filter rows before it sends them — =, !=, >, in(). Where a backend supports them, Vantage pushes the filtering down to it. Does the Kubernetes API take conditions like these? Only the plainest — a simple = on a few fields. So most of this step is skipped, and the rest happens after fetching. expand_moreSee what's skippedHide detail
Operators — typed conditions, not raw strings

Once a backend can run conditions, Vantage layers ergonomic, typed operators onto columns — .eq(), .gt(), .in_() — each producing that backend's native condition type. How far they reach is up to the backend: SQL takes arbitrary expression conditions, a document store maps them to its own operators, and some sources support little more than equality. Each DataSource declares and implements exactly the conditions it can honour — Vantage never pretends a backend can filter in ways it can't.

Rust
table.add_condition(status.eq("active"));
table.add_condition(price.gt(100));
Rhai
.where(row["status"] == "active")
.where(row["price"] > 100)

Not needed for Kubernetes — the API has no general query language to push a condition into, so the agent filters the fetched list in memory instead. Skipped.

Read furtherarrow_forward

4Step 4 · Query builder · skipped A query language lets you compose a precise request — pick columns, narrow rows, sort, even nest queries inside queries. Where that exists, Vantage assembles it properly. Can the Kubernetes API build a query like that? No — it just returns a whole list of resources. So this last query step is skipped too. expand_moreSee what's skippedHide detail
Query builder — composable SELECTs

For backends that run queries, Vantage wants them implemented properly, not faked. It introduces the Selectable trait — the standard SELECT-builder interface — and the agent implements it for the backend's own statement type. The variants are SQL-like and compose: fields, conditions, ordering, limits, and nested sub-statements. Once a DataSource is Selectable, the builder is safely exposed in both Rust and Rhai:

Rust
SqliteSelect::new()
    .with_source("product")
    .with_field("name")
    .with_field("price")
Rhai
select()
    .from("product")
    .field("name")
    .field("price")

Not needed for Kubernetes — a resource list comes back whole, with no SELECT to build, so the agent filters it in memory. Skipped.

Read furtherarrow_forward

5Step 5 · Table & CRUD So far I've focused on individual capabilities; now I assemble them together, through the power of Vantage Tables. A Table is a collection of records you can create, read, update or delete — CRUD. For Kubernetes those tables are quite specific: Pod, Node, Service, Deployment and the rest — the resources you actually browse. expand_moreHow it's builtHide detail
Where everything converges

Here the earlier pieces come together. The agent implements one trait, TableSource, and on it declares the backend's types — its value type, its condition (comparison) type, its select/source type, and several more. The same trait spans everything from a flat CSV to SurrealDB, one of the most feature-rich database servers; each backend simply names the types it actually has.

PostgreSQL
impl TableSource for PostgresDB {
    type Value = AnyPostgresType;
    type Condition = PostgresCondition;
    type Source = SelectSource<PostgresSelect>;
    // + Column, Id, AnyType …
}
Kubernetes
impl TableSource for KubernetesCluster {
    type Value = CborValue;
    type Condition = KubeCondition;
    type Source = String;
    // no query language — just the kind
}

Postgres declares a full Select-based source; Kubernetes, with no query language, declares its source as a plain String — the resource kind. That single line is exactly why the three query steps above were skipped.

Implementing TableSource unlocks a Table: a typed Table<DB, Entity> with ActiveRecord-style records, CRUD, aggregates, ordering and pagination — none of which you write. Table is Rust-only and highly typed; in a Rust application it's what you reach for most of the time.

Reading it back is exactly what you'd expect — load the pods table and iterate its rows:

// load the pods table, then read every row
let pods = pods::pods_table(cluster);

for (_, pod) in pods.list_values().await? {
    let name  = pod.get("name");
    let ready = pod.get("ready");   // "1/1", "2/2", …
}

Read furtherarrow_forward

6Step 6 · Relationships Tables don't exist in a vacuum — they're connected. A deployment is made of pods; a pod lives on a node. Vantage links tables with references, and in the UI those turn into drill-downs and sub-lists. In this step I connect all the base tables correctly, so an expression like node.ref("pods") resolves to exactly the right rows. expand_moreHow it's builtHide detail
Switch on the relationship engine

Now Vantage's relationship engine comes on. You declare how records relate — with_one for a belongs-to, with_many for a has-many — and the UI picks them up on its own, turning every reference into a link you can follow, with no per-screen wiring.

Table::new("replicasets", cluster)
    .with_many("pods", "owner_uid", Pod::table);  // a ReplicaSet → the pods it owns

How much a relationship can do depends on the backend. Vantage supports three levels — and since pushing work to the server is always cheaper, it prefers the higher ones. Pick a level:

Server-side conditions and subqueries. A relation can carry expressions and aggregation — a client's order_count computed as a correlated subquery on read. launch-control builds its aggregation expressions exactly this way:

.with_many("orders", "client_id", Order::table)
.with_expression("order_count", |t| {
    let orders = t.get_subquery_as::<Order>("orders").unwrap();
    orders.get_count_query()
})

Most REST APIs filter on the server, but only while fetching — folded into the path or query string, never as a subquery inside another query. Kubernetes lives here, with label and field selectors and owner-reference ids — enough for relationships to resolve cleanly:

  • a deployment has many pods
  • a node has many pods
  • a namespace has many services
  • a pod has one node

A CSV or a CLI can't filter at all, so Vantage fetches what it can and narrows the results in memory afterwards — the same link still resolves to exactly the right rows, just at the client.

Read furtherarrow_forward

7Step 7 · Checkpoint Time to check the work so far. The guide prompts me to build a simple command-line utility and test it against a real Kubernetes cluster (minikube) — verifying the types, tables and relations I've built. For a more advanced data source this checkpoint would also cover expressions, custom queries and advanced conditions — but Kubernetes has none of those, so the essentials are enough. expand_moreSee it runHide detail
Prove it from a CLI

By now the data source has the whole stack — types, expressions, conditions, queries, a typed Table and relations. The guide's checkpoint is to build a small CLI, the fastest way to exercise all of it against a real cluster. Because the backend type is erased, the same generic CLI code runs over any Vantage model: it fetches, filters with conditions and traverses relations, and vantage-cli-util renders each result as a table.

Fetch a resource and filter it by namespace — vantage-cli-util renders the result straight into the terminal:

k8s-cli listing the demo namespace's pods as a coloured table — pi, sidecar (2/2) and three web replicas

Then traverse a relation — a deployment down to the pods it owns:

k8s-cli drilling apps.deployment name=web down to its three web pods

Exactly the three pods that deployment owns — fetched, filtered and drilled, with no Kubernetes-specific code in the CLI at all. The checkpoint passes.

Read furtherarrow_forward

8Step 8 · Vista So far this is perfect for a custom Rust project — but Vantage doesn't stop here. It turns to type-erasure, which lets tables be defined on the fly and connected to UI elements and API endpoints. The way Vantage seals a table like this is called a Vista. With a Vista I can be honest about what each table can actually do — and given that reality check, Vantage auto-implements the missing capabilities on-the-client. expand_moreHow it's builtHide detail
Seal the types — make it universal

So far everything has been type-rich Rust resources — ideal for Rust code, and invisible to everything else. To use that Table — the DataSource you just built — outside of Rust, or in Vantage UI, those types have to be sealed behind one uniform boundary. That boundary is Vista: a typed Table goes in, and out comes a schema-bearing handle the UI, scripts and agents read over a plain serialization boundary — no Rust struct needed on the other side.

As the table enters Vista it also declares the backend's capabilities. Queries? No. Conditions? Yes. Pagination? Yes. Consuming code reads these flags and adapts, so the same UI drives a SQL Vista, a Mongo Vista or this Kubernetes one without caring how it was built. Here is what the Kubernetes driver advertises:

SupportedUnsupported
countfetch the total number of rows
ordersort by any column
searchquick free-text search
set_page_sizechoose how many rows a page holds
fetch_pagerandom-access pages by number
fetch_nextcursor-style pagination
fetch_windowoffset windows for lazy scroll
insert / update / deletecreate, edit or delete resources
subscribelive updates as the cluster changes
traverse_to_setnarrow a relation with a subquery
build_ref_via_scriptRhai-scripted reference traversal

Some of the right-hand column are real limits — the API has no query language, so server-side queries will never appear. But others the Kubernetes API does support; the v1 driver just doesn't use them yet — a live watch, server-side field selectors. They can be added later to make the driver more powerful, and swapped in without touching a line of code that uses Vantage — Vantage UI included.

Read furtherarrow_forward

9Step 9 · Scripting Vantage UI ships as one prebuilt app, so I can't add fresh Rust to it. Instead, tables are declared at runtime — in plain YAML or short Rhai scripts it reads as it runs. That's the real unlock: with no rebuild, you can point Vantage at your own Kubernetes resources — even custom ones — and add them to the UI yourself. expand_moreHow it's builtHide detail
Declare tables without Rust

Vantage UI ships as one precompiled binary — you can't drop new Rust into it. So tables are declared in YAML or Rhai instead, both read at runtime. Here's the same deployments table, three ways:

# table/deployments.yaml
datasource: cluster
title: Deployments
table: apis/apps/v1/deployments
columns:
  - { name: name, flags: [title] }
  - { name: namespace, references: namespaces }
  - { name: replicas, type: int }
  - { name: ready, type: int }
references:
  pods: { table: pods, kind: has_many, foreign_key: ownerDeployment }
# the pods relation, narrowed by a Rhai script
references:
  pods:
    table: pods
    kind: has_many
    rhai: |
      table("pods")
          .add_condition_eq("ownerDeployment", row.name)
          .add_order("name", "asc")
Table::new("deployments", cluster)
    .with_title_column_of::<String>("name")
    .with_column_of::<String>("namespace")
    .with_column_of::<i64>("replicas")
    .with_column_of::<i64>("ready")
    .with_many("pods", "ownerDeployment", pods_table);

Two of those need no recompile — which is the real unlock: you can mix and match custom manifest types, pointing a table at a CRD to add it to Vantage UI without touching the binary.

The Kubernetes driver itself shipped in Vantage 0.20 and stays a built-in capability — so a cluster works out of the box, and YAML or Rhai is for the custom types you layer on top.

Read furtherarrow_forward

Claude Opus 4.8 implemented every step — and added the tests too — everything fitting Vantage's specifications, from a single, simple prompt.

This ran against Vantage 0.19; the finished driver shipped in 0.20 a few days later and is now built in. So the ~2,300 lines of protocol code were a one-time cost — rebuild Periscope today and it's just the ~800-line app. vantage-kubernetes lives in github.com/romaninsh/vantage alongside several other incubating crates, so a more refined driver can later take over without changing a line of the app.

Vantage. Most modern frameworks are built by humans, for humans. Vantage is built by humans too — but aimed squarely at AI and human developers alike.

The new reality is that code is cheap: quick to write, and quick to rewrite. A sharper Kubernetes driver may already be on its way — and that's fine, because swapping a DataSource has little to no impact on everything built on top of it.

So whatever legacy or in-house API your organisation runs, you can give it a data source — and a real, browsable UI — with a single prompt, today. PR #323 is exactly that, in action.

Prompt two

Built into Vantage UI

The work so far was a bit of a detour. Chances are Vantage UI already supports the database — or API protocol — you use; Kubernetes was the exception. Speaking it needed a brand-new DataSource, wired by hand into Vantage UI's single, static binary — so that every user benefits from the new integration, not just us.

That wiring is a one-off, and it's mostly about shipping the Skills and Schemas that teach Vantage UI to use the driver. One prompt handles it:

smart_toyPrompt 2 · Claude Opus 4.8

add support for kubernetes to vantage-ui, using work you've done so far, exposing all capabilities, write the skill files and update version to 0.20

Integration in ~5 minutes — plus ~20 to run the production-release pipeline.
Vantage UI simply bundles open-source components and builds a UI around them. Adding a new data source is short, minimalistic work — a new dependency, and a safe authentication mechanism. Romans · Vantage

From there the change rides the same release pipeline as every Vantage build:

  1. Verify the DataSource. A suite of generic tests confirms the new implementation behaves like every other backend.
  2. Build a nightly. Vantage's rapid release channel publishes builds often, so the driver lands fast.
  3. BDD test-suite. An interactive behaviour suite checks the finished application against the sample apps.
  4. Release. The final pipeline publishes, and installed apps auto-update.

For commercial partners we produce dedicated builds — the public version mirrored, with custom DataSources and tweaks added — running through the very same release process and automated testing.

Prompt three

The control room, in YAML

With the driver shipped in 0.20, building the app needs no setup of our own. We run Vantage 0.20 and follow its prompts: the Add data source dialog offers to set things up with AI, and the wizard installs the right Skills — the agent's instructions — into a fresh project.

Vantage's Add Datasource dialog offering to set up a data source with an AI agent
Add data source → Set up with AI.
The Set up with AI wizard, picking the kinds of data and installing agent skills into a project
The wizard installs the Skills into a new project.

Then we open that project folder with an AI agent and give it a single sentence:

smart_toyPrompt 3 · Claude Opus 4.8

Build a Headlamp clone and test it with local minikube

32 YAML files · zero lines of UI code.

The agent wrote the whole control room as YAML — a dozen resource tables with drill-down relations, two live dashboards, bespoke Summary panels and a grouped, themed sidebar — then ran it against a local minikube cluster to check its work:

Periscope's Usage dashboard — live CPU and memory by node and by pod, with a namespace selector
The usage dashboard — live CPU/memory.
The Deployments grid with a rollout Summary view — health badge, ready-replicas progress bar
A deployment's Summary — badges, rollout bar.
The ConfigMaps list with a master/detail binder panel showing a selected ConfigMap's fields
ConfigMaps in a master/detail binder.
verified All 32 YAML files pass the real schema-validating loaders, and a run against a live minikube cluster came up clean — 0 WARN, 0 ERROR: connected kubernetes cluster datasource=cluster, dashboards bound to live node data. Read PR #17.

The result

The treasure at the bottom

There's no chest down here. The treasure at the bottom of the dive is what three prompts produced: a native Kubernetes datasource, a UI that speaks it, and a full control room over a real cluster — zero lines of UI code, no kubectl. Two of those prompts were one-offs, and they've shipped: the driver is built into Vantage 0.20, so today the control room is just the third prompt — an afternoon's work, not a quarter's. The prize was never the app. It's the speed.

And it generalizes. Because Vantage is AI-ready end to end, you can point an agent at its guides and turn any backend — an API, a database, a CLI — into a drillable, related-table app. Periscope is what that looks like when the backend is a cluster. Yours could be anything you operate.

Surface with it

Read the source, follow the three prompts that built it, open it in Vantage 0.20 to drive your own cluster, or ask for a custom build for your own data sources.