Estate shows the state of a set of services on one page.
When something breaks in production, the information you need to look into it is usually spread across several systems. An alert might be in Grafana, the logs in Elasticsearch, the deployment in GitHub or Jenkins, the workload in Kubernetes, the owner in a service catalogue, the runbook in Confluence, and the people looking at it in Slack.
Estate reads those systems and shows what they say about each service on one page that updates as things change, with links to each system for the detail.
Estate does not replace Grafana, Datadog, Elasticsearch, Kubernetes, GitHub or other tools, and it does not store their data. If all your metrics, logs, deployments, incidents, owners and runbooks are already in one platform, you probably don't need it.
It is meant for setups where teams use a mix of tools: one service on Prometheus and Grafana, another on Datadog or CloudWatch, logs in Elasticsearch or Loki, deployments through GitHub Actions, Jenkins, Harness or Argo CD. Looking into an alert in a setup like that often means going from the alert to metrics, logs, the latest deployment, the repository, the owner, Slack and the runbook in turn.
For each service, Estate shows:
- alerts, thresholds, impact and history
- what changed: deployments, builds, alerts, silences and notes
- what's running, from Kubernetes and the deployment tool
- request rate, errors, latency and other signals
- the owning team, repository and contact details
- links to logs, dashboards, traces, runbooks and the source systems
Say an alert called OrdersSlow fires at 11:46. Its page in Estate shows:
- what's happening: a warning, firing for 14 minutes, and its impact (customers wait to place orders)
- what's around it:
orders' new version stalled 20 minutes before the alert, because its image policy could not list tags;orders-dbis healthy at 32% of its connections; there have been 128 "lock timeout on orders_items" errors since just before it fired; the runbook says to roll back if p99 rose after a deploy - who owns it: the Orders team, with a link to their Slack channel
- if a model is configured, Ask AI gives a likely cause, the lines of the brief that support it, and what to do next
| Alerts | Alertmanager, Grafana alerting, CloudWatch alarms, Datadog monitors |
| Metrics | Prometheus (or anything with its query API), Datadog, CloudWatch |
| Runtime | Kubernetes, ECS |
| Deploys | Flux, Argo CD, Harness CD, ECS deployments |
| Builds | GitHub Actions, GitLab CI, Jenkins, TeamCity, Harness CI |
| Logs | Loki, Datadog, Elasticsearch/OpenSearch, or pod logs straight from the cluster |
| AI agents | Runs, failures, tokens against a budget and the model in use, from the GenAI metrics they already send; recent runs from Langfuse |
| Costs | AWS Cost Explorer by tag, with forecasts and anomalies; OpenCost or Kubecost for what shares a cluster; Anthropic's and OpenAI's cost reports for agents |
| Code health | SonarQube's quality gate and coverage; GitHub's Dependabot and code scanning alerts |
| Notes, impact and alert history | Postgres, DynamoDB, or memory |
Where Grafana sits in front of Prometheus and Loki, Estate can reach them through Grafana with one service account token instead of separate addresses and credentials. The same page works on other stacks, for example EKS with Grafana's alerting, Argo CD and Elasticsearch, or an environment read entirely from Datadog and built by Jenkins.
Estate reads data from your tools each time it shows it and does not copy it. The only things it stores are what it adds itself: notes, what an alert means, and each alert's past firings.
- add a note to an alert so the next person knows someone's on it
- say what an alert means for the people using the product, kept for every time it fires
- see whether an alert has fired before, for how long, and what was written about it then
- silence an alert for a while, with a reason everyone can see
- turn on debug logging for a service for 15 minutes; it switches itself back off
- watch a service's logs live, filter them by level or text and pause them, or see its errors grouped by message
- read a service's load and stats over the last hour, six hours, day or week; point at a chart to read the same moment on every chart, and drag across one to zoom them all
- compare what's deployed in each environment
- open an alert's own page to share: what's happening, what is around it, who owns it
- ask AI about an alert, when a model is set in
estate.yaml - raise an incident in PagerDuty or incident.io from its page or an alert's card, when
incidentToolis set inestate.yaml; the incident then links to it there, its timeline says who raised it, and its page says what the tool says of it: its status, its priority or severity, and who is on it - otherwise, open your incident tool's own form from the alert, when the catalog names its link
- tell the team that owns an alert in its Slack channel, once a firing; the card then links to the thread
- jump to any service, store, job or agent by name from the header, or with ⌘K (Ctrl K)
- see a service's version in every environment at once, and switch to one
- see what each service, job and agent costs this month against its budget; a cost anomaly or a budget it is on course to pass is flagged, and an agent's spend between its provider's reports is estimated from its tokens and marked as an estimate
- see each service's code health under its name: its quality gate, coverage and open security alerts, shown in amber when its owner should look, each linking to SonarQube or GitHub
- follow an AI agent's runs, failures and tokens against its daily budget, and open its recent runs
- see when a job last ran and when it runs next, and a store's stats over a day
Viewers see everything and add notes. Operators can also silence, switch debug, and write what an alert means.
The overview counts what needs you one way: an open incident with its alerts, an alert in no incident, a deploy stuck in the environment, and a service or store that is degraded with nothing else about it are each one thing, and the headline, the list and the tiles all count those. With an incident open, the top of the page says what it means for users, its likely cause where its alerts point at a store or one fired first, and who last wrote on it, and below the list is how it unfolded, its alerts and notes oldest first. Each thing is a row that opens its alerts' cards in place.
Services, stores and jobs can be grouped by category, such as Payments or Data, so a page of forty services
can be read as five groups, those that need someone first. In each, the healthy services and stores are one line of
links until you open their lanes. The map is on Auto unless you choose: past a dozen services (map.areas in the
catalog) or on a phone it is a card per area with every part named, what needs attention first, and where traffic meets
trouble; otherwise it is drawn, a node per category once there are more than a dozen nodes (map.collapse), opened in
place when you click it. The overview's View menu chooses the map's layout and whether services are lanes or a grid of
compact cards; each choice is kept in this browser. On a service's page, its charts mark when a version was applied
and when an alert fired.
An alert's own page collects related information without using AI: the deploys and builds of its service and of what it calls or what calls it, from the hour before it fired; how those neighbours are, with their stats; its errors from ten minutes before it fired, grouped by message; its earlier firings and what was written then; and its runbook's text, where the runbook is a page Estate can read.
If a model is configured, every alert's card has Ask AI, on the overview, on Alerts and on the alert's own page. It
gives that brief to the model, which may call the same read tools agents use over /mcp for what the brief does
not say, and answers on the card with the likely cause, the evidence for it, and what to do next. A bar shows it is
reading until the answer comes.
The answer lists what it read and called, so the reader can check it, and what it took in tokens, with an estimate
of its cost where prices names the model. You can keep the answer as a note for
everyone to see. The model is sent only what the person asking could see on the page. Where auth.logs: operator
keeps logs to operators, an answer that read them shows on a viewer's timeline only as having answered, and keeping
it as a note says that everyone will see it. There is at most one answer
per alert per minute, within a daily token budget.
# estate.yaml: one model
ai: { provider: anthropic, apiKey: "${ANTHROPIC_API_KEY}", model: claude-opus-5-5, budget: { tokensPerDay: 500000 } }
# or provider: openai, xai or gemini with their key, or a self-hosted server that speaks OpenAI's API:
# ai: { provider: openai-compatible, url: http://vllm.internal:8000/v1, model: my-model }
# What an ask may spend: how hard the model thinks (unset unless given; low is cheapest, and a model that does not
# take it is asked without), rounds of read tools (2), and how much of one tool's result it reads (4000).
# effort: low, toolRounds: 2, toolResultChars: 4000
# Credentials for reading runbooks' text, by the host their links name. A file on GitHub is read as the file: from
# raw.githubusercontent.com, or with github.com's token through GitHub's API, for a private repository.
runbooks:
- { host: wiki.example.com, user: estate@example.com, token: "${WIKI_TOKEN}" }
- { host: github.com, token: "${GITHUB_TOKEN}" }Estate serves an MCP server at /mcp, so an AI agent such as Claude Code can read
the same information a person sees on the page. Access is read-only and uses Estate's own access to the tools. Each
agent has its own token.
# estate.yaml
mcp:
tokens:
- { name: claude-code, token: "${ESTATE_MCP_TOKEN}", role: viewer } # at least 32 charactersclaude mcp add --transport http estate https://estate.example/mcp --header "Authorization: Bearer $ESTATE_MCP_TOKEN"| Tool | Answers |
|---|---|
estate_now |
what needs someone in an environment, worst first |
services |
every service, store, job and agent: health, reasons, version, owner, category |
service |
one service: health, load, pods, deploy and builds, alerts, links, team |
alerts |
what is firing, pending and silenced, each with its impact, notes, silence and runbook |
alert_history |
an alert's earlier firings, with who silenced each and why, and the notes written then |
changes |
what changed today: deploys, builds, alerts, silences, notes and jobs, newest first |
errors |
a service's errors over an hour, six hours or a day, grouped by message, for a token whose role may read logs |
agents |
each AI agent's runs, failures, tokens against its budget and its model |
around_alert |
an alert's brief: what changed near it, what it depends on, its errors, history and runbook |
/kiosk is meant for a wall screen. It shows the headline, what's firing and every service, worst first, in large
type and with no controls. Open /kiosk?token=… once on the screen; it signs in for 30 days and can
read but never change anything. A screen that shows Estate inside a dashboard's frame needs that dashboard's origin
in web: { frameAncestors: [https://grafana.example.com] }; otherwise Estate refuses to be framed by another site.
docker run -v ./examples:/etc/estate -p 8080:8080 ghcr.io/matthewjones372/estate:mainThen open http://localhost:8080. The example points at tools that don't exist, so each part of the page says
what it couldn't reach. Edit examples/estate.yaml to point it at your own.
To see an estate with something wrong in it, without any tools of your own, run the demo from a clone:
bun install
bun examples/demo/run.tsIt starts Estate on http://localhost:8080 against one process playing every tool it reads, for Larder, a made-up
online grocer. Staging is well. In production a broker of the six-broker Kafka cluster is gone, a consumer group is
falling behind, a database replica is behind, checkout's new version is slow, an incident is open with comments in
it, traffic rises and falls with a lunchtime rush and a newsletter, and you can follow an order from the web to each
group that reads it. Ask AI answers there are canned, and say so. Everything comes from
examples/demo/: its catalog is a real one to read and copy from.
To see it with live data, petshop's demo runs Estate
in Docker Compose against a real Prometheus and Alertmanager watching a small Kotlin service. Stopping the demo's
chip registry fires an alert, which appears at the top of the page and resolves once the registry is back. Its
estate/ folder is a working estate.yaml and catalog.yaml to start from.
For a bigger example on Kubernetes, tweet-street runs Estate on a
local kind cluster beside four services and their databases, reading Prometheus, Alertmanager and the cluster itself,
with Estate running as a cluster of its own. scripts/up.sh in its lark-bank/ folder brings it all up, and its
deploy/k8s/estate/ folder
has the settings and catalog.
To try it against your team's real tools, set readOnly: true in estate.yaml so Estate writes nothing (no
silences, no debug switching, notes only in memory), then run the doctor command:
docker run -v ./my-estate:/etc/estate --env-file my-estate/.env ghcr.io/matthewjones372/estate:main doctorIt asks each tool once and prints a line per part: what answered, how many alerts matched a service, which services have no running pods or no log lines, and which setting would fix it. docs/real-tools.md has the settings for each tool, and what each line should say when it's right.
Estate reads two files from /etc/estate. catalog.yaml says what your estate is, and lives in Git, so adding a
service is a pull request:
environments:
- { name: production, sources: production } # sources: which section of estate.yaml has its tools
teams:
- name: payments
title: Payments
links: { slack: "https://example.slack.com/archives/C0PAY", oncall: "https://example.pagerduty.com/schedules/PPAY" }
services:
- name: payments
owner: payments
category: Payments
environments: [ production ]
kubernetes: { namespace: shop, workloads: [ { kind: Deployment, name: payments } ] }
deploy: { flux: { kustomization: shop, imagePolicy: payments } }
build: { github: { workflow: payments.yml, branch: main } }
load:
requests: sum(rate(http_requests_total{app="payments"}[1m]))
p99: histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{app="payments"}[5m])))
links:
logs: "https://grafana.example.com/explore?var-env={env}&var-service={service}"
traces: "https://grafana.example.com/explore?var-service={service}"
# Raise incident on its alerts' cards, where estate.yaml sets no incidentTool: Estate opens the tool's own form
incident: "https://example.pagerduty.com/incidents/create?service={service}&title={alert}"
alerts:
PaymentsFailing: { impact: "Customers cannot pay; orders wait in their baskets." }estate.yaml holds the settings: sign-in, roles, where each environment's tools are, and Estate's own database for its notes and history.
Secrets are written as ${NAME} and read from the environment.
estate check catalog.yaml reports every mistake in one go, with where it is, so it fits in CI. Estate reloads the
catalog when it changes.
If most entries would look the same, a discover: rule in the catalog can have Estate find them instead. It
matches Deployments and StatefulSets by label selector and turns each into the entry the rule describes, taking the
owner and category from labels and the description, repository and runbook from estate.dev/ annotations. Found
entries are checked the same way as written ones and marked "found in Kubernetes" on the page, which offers their YAML
to copy into the catalog. A written entry with the same name takes priority.
A backstage: rule does the same from Backstage's catalog. Each Component becomes a service, with its owner, its
system as category, description, links, and repository and namespace from the annotations Backstage's plugins use.
The Backstage address and token go in estate.yaml.
A kafka: rule finds Kafka clusters in each environment's Prometheus, one for each set of values of the labels it
names on kafka_exporter's series. Each cluster becomes a Kafka store, selected by those labels and filled from the
rule's store:, and is marked "found in Prometheus".
discover:
- kubernetes: { selector: estate.dev/show=true, namespaces: [ shop ] }
service:
owner: "{label:team}"
category: "{label:app.kubernetes.io/part-of}"
load: { requests: 'sum(rate(http_requests_total{app="{name}"}[1m]))' }
- kafka: { by: [ namespace, strimzi_io_cluster ] }
store: { name: "{label:strimzi_io_cluster}", category: Data }examples/catalog.yaml uses every option, including stores, jobs no service owns, the map
and vitals.
Estate is used alongside these tools and links into each of them.
- Backstage, Port and Cortex catalog what you own. Estate shows its current state.
- Grafana can chart anything, but someone has to build and maintain the dashboards, and a dashboard does not know who owns a service, what was deployed before an alert, or what was written the last time it fired. Estate links to Grafana for detail.
- k9s, Lens and Headlamp show one cluster in depth. Estate covers several environments and links into them.
- Argo CD's UI and Weave GitOps show the deploy tool. Estate shows that next to the build and what's running.
- Karma and Keep handle alerts by themselves. Estate shows each alert next to the service it's about.
Images for amd64 and arm64 are published to ghcr.io/matthewjones372/estate from main. deploy/ has a
Kubernetes base to overlay with your two files, an ingress and your secrets. It runs as one container in about
120 MB.
- Sign-in is OIDC with PKCE, with groups mapped to viewer and operator. Anonymous sign-in is available for trying it out.
- Health:
/healthzsays the process is up, and/readyzsays every source has been read at least once. - Estate's own telemetry: metrics on
:9464/metrics, JSON logs, and traces over OTLP if you set a collector.
One replica is enough for most setups, since it restarts in about the time a failover takes. Run more if the tools'
rate limits are being hit, if losing a node must not blank the page, or if many kiosks outgrow one process. Set
cluster: true beside database: { postgres: … } and add the deploy/cluster component to your
overlay. Every replica serves the pages. Each environment is read by one replica, and Estate's own work (its catalog,
builds, notes and history) by one replica too, so the reading is spread between replicas and two replicas make the
same number of calls as one. A note or silence made on any replica shows on all of them. When a replica goes, the others take what it read
within a minute, and the pages keep the last state meanwhile. /readyz says what a replica reads and whom it follows
for the rest, and estate doctor names the runners and who reads the estate and each environment. examples/cluster runs two on one
machine.
flowchart LR
tools["Your tools<br/>Prometheus · Alertmanager · Kubernetes<br/>Flux · GitHub Actions · Datadog · …"]
sources["Sources<br/>one reader per tool, each on its own interval"]
state["Estate's state<br/>the current estate, in one place"]
web["Pages<br/>Solid"]
tools --> sources --> state -- "server-sent events" --> web
Each tool is a source behind a typed port, with a fake for tests. Sources do the talking, retries and timeouts.
Estate holds the current state, and each environment's views are built once and shared by every page watching it.
A new page receives a snapshot, then only what changes; a page that reconnects resumes from its Last-Event-ID.
The server uses Effect, because most of the work is remote calls that can fail, along with retries, timeouts, schedules, concurrency and long-lived streams. With Effect, a tool being down is part of the typed error model instead of an exception thrown somewhere in a request. The pages use Solid, which redraws only what changed.
flowchart LR
web["src/web"] --> shared["src/shared<br/>schemas both sides decode with"]
server["src/server"] --> shared
The server and the pages never import each other, and shared imports nothing of ours. Those boundaries are
checked on every build.
Fifty services is about the most a team puts on one page, and the build fails if Estate gets slower at that size.
A thousand is a stress test. Both are measured by bench/ against fake tools that answer in 20 ms:
| 50 services, 5 pages | 1,000 services, 20 pages | |
|---|---|---|
| Calls to the tools a second | 11 | 101 |
| First data to a page | 0.1 MB | 2 MB |
| Estate's CPU | 3% of a core | 52% |
| Estate's memory | 136 MB | 174 MB |
| Page drawn | 0.5 s | 2.2 s |
| Clustered: whole estate to a follower | 186 KB | 3.7 MB |
| Clustered: changes to a follower a minute | 27 KB | 485 KB |
Every page watching an environment shares one stream, so more pages cost little. Each source is read on its own
interval, which every: lengthens for tools that limit or bill each call.
bun install
bun run gate # typecheck, lint, unused code, layering, slop, tests with coverage
bunx playwright test # the pages in a browser against fake tools, with axe accessibility checks
bun run perf # fifty services against the budgets
bun run integration # Estate's readers against the real tools in containers: needs DockerA change is not done until the gate passes. The gate runs strict TypeScript, Biome, Knip,
dependency-cruiser, and the tests, which must cover 90% of the lines in every file. It also refuses any, non-null assertions,
console, TODOs, commented-out code, skipped tests, files over 300 lines, rules switched off inline, and running an
Effect inside application code. The same gate is the agent's stop hook. bun run integration runs the readers
against real Prometheus, Alertmanager, Grafana, Loki, Elasticsearch, Postgres, DynamoDB, Jenkins, LocalStack and
Kubernetes.
AGENTS.md covers code conventions. Every change starts as a spec in specs/.
- The state of each service should be visible on one page, without opening several dashboards.
- Estate is not a system of record. Prometheus holds metrics, Alertmanager holds alerts, Kubernetes holds workloads and Git holds configuration. Estate reads from them.
- The catalog is written explicitly and reviewed in Git. It can ask for services to be discovered, and anything written in it takes priority.
- When a source stops answering, the page says which one and since when, instead of showing old data as if it were current.
- The integration work happens once on the server for every page, so the browser code stays simple.
- Reading and changing are separate, and every change records who made it and why.






