A lakehouse built to be looked at. Every figure on this page was read from the running machine when the page was generated — nothing here is typed by hand.
Drawn when the page was generated. The layout is design; the dots and the control-plane figures are read from the machine, and change with it.
Single host, single instance of everything. No high availability, and eight cores is the ceiling — this demonstrates design, not scale.
The version of every engine is pinned in one file and nothing else in the repository may name one. This table compares what is declared there against what is actually running on the box right now. Disagreement is reported, not hidden — that comparison is the platform's whole job, and it applies to itself first.
| Component | Choice | Pinned | Running |
|---|---|---|---|
| Object store | Garage | 2.3.0 | running |
| Table format | Apache Iceberg | 1.11.0 | not a service |
| Catalog | Apache Polaris | 1.7.0 | running |
| Query engine | Trino | 483 | running |
| Metadata store | PostgreSQL | 18 | running |
| Streaming | Redpanda | 26.2.2 | running |
| Transform | dbt-trino | 1.10.3 | not a service |
| Orchestration | Dagster | 1.13.21 | running |
| Control plane | registry, contracts, freshness | — | running |
| Query console | bounded public SQL | — | running |
| Name | Image | State |
|---|---|---|
| openclaw-uy0y-openclaw-1 | ghcr.io/hostinger/hvps-openclaw:latest | Up 28 hours |
| platform-console | platform-jobs:current | Up 4 days (healthy) |
| platform-controlplane | platform-jobs:current | Up 4 days (healthy) |
| platform-dagster | platform-jobs:current | Up 4 days (healthy) |
| platform-dagster-daemon | platform-jobs:current | Up 4 days |
| platform-garage | dxflrs/garage:v2.3.0 | Up 4 days (healthy) |
| platform-polaris | apache/polaris:1.7.0 | Up 4 days (healthy) |
| platform-postgres | postgres:18 | Up 4 days (healthy) |
| platform-redpanda | redpandadata/redpanda:v26.2.2 | Up 4 days (healthy) |
| platform-site | nginx:1.31.5-alpine | Up 4 days |
| platform-trino | trinodb/trino:483 | Up 4 days (healthy) |
| traefik-traefik-1 | traefik:v3.7.13 | Up 4 days |
Everything stateful lives under one root, so a backup has exactly one thing to walk. These directories exist; most are empty until the substrate is deployed.
| Path | Size |
|---|---|
| /data/garage/meta | 1.6M |
| /data/garage/data | 133M |
| /data/postgres | 72M |
| /data/polaris | 4.0K |
| /data/trino | 6.7M |
| /data/redpanda | 180M |
| /data/dagster | 1.5M |
| /data/backups | 3.9M |
| /srv/platform | 6.0M |
One public door. Every other component binds to the Docker network or the private tailnet and carries no routing label at all — a container with no label is not reachable, and that default is the security model rather than a firewall rule somebody has to remember.
| Port | Process | Reachable from |
|---|---|---|
| 80 | traefik | the internet |
| 443 | traefik | the internet |
| — | everything else | private network only (10 local listeners) |
The engines above are other people's work. This is the part that is built here: every dataset is declared in a reviewable file — owner, purpose, schema, freshness SLA — and reconciled against what Polaris and Iceberg actually report. Disagreement is recorded and dated, not corrected. A platform that quietly makes reality match the paperwork destroys the evidence that they ever diverged.
Every dataset, and who is accountable for it. A dataset with no owner is a dataset nobody is going to fix.
| Dataset | Owner | Domain | Class | Purpose |
|---|---|---|---|---|
| demo.nyc_trips | rixon | mobility | public | Public NYC TLC yellow taxi trip records, landed as Iceberg. The large, genuinely messy dataset -- partitioning, compaction and query planning matter h |
| demo.streamed_orders | rixon | operations | synthetic | The same order events as the batch feed, arriving one at a time through Redpanda instead of a file at a time. It exists to answer a specific question: |
| demo.synthetic_orders | rixon | operations | synthetic | A synthetic operational feed, generated by data-weaver. It exists to be caught misbehaving: anyone can show a green dashboard, and a quality plane wit |
| demo.trips | rixon | platform | synthetic | The Phase 1 milestone: three rows written by hand (sql/phase1-milestone.sql) to prove a month-partitioned Iceberg table could be created in Garage thr |
| marts.trips_daily | rixon | mobility | public | Daily trip totals over the ingested window. The first thing anyone actually asks of trip data, and the first thing that becomes wrong if an ingest dou |
Each dataset declares how stale it is allowed to become. Late is a state, reported explicitly — not the absence of a green tick.
| Dataset | SLA | State | |
|---|---|---|---|
| demo.nyc_trips | 1080h | meeting-sla | |
| demo.streamed_orders | 6h | meeting-sla | |
| demo.synthetic_orders | 36h | stale | newest snapshot is 1074.5h old; the declared SLA is 36h (1038.5h over) |
| demo.trips | — | static | |
| marts.trips_daily | 1080h | meeting-sla |
Iceberg tables accumulate small files and snapshots with every write. They are compacted and expired weekly, and these figures are read from each table's own metadata at every reconciliation — so when maintenance stops working it shows up here as a finding, not as a slow query months later.
| Dataset | Rows | Snapshots | Files | Avg file |
|---|---|---|---|---|
| demo.nyc_trips | 9,308,866 | 1 | 9 | 14.1 MB |
| demo.streamed_orders | 92,000 | 11 | 46 | 36 KB |
| demo.synthetic_orders | 26,000 | 1 | 2 | 57 KB |
| demo.trips | 3 | 1 | 2 | 1 KB |
| marts.trips_daily | 60 | 1 | 1 | 3 KB |
What the platform found on its own. It has never been told what to look for: the synthetic feed's defects are declared in a file the detection path may not read, so anything below was found the way a real problem would be.
| Severity | Dataset | Finding | Detail |
|---|---|---|---|
| breaking | demo.synthetic_orders | stale | newest snapshot is 1074.5h old; the declared SLA is 36h (1038.5h over) |
| breaking | demo.synthetic_orders | type-mismatch | 'amount' is declared double but is actually varchar |
Assertions declared per dataset and run on a schedule. Each returns how many rows violate it rather than a yes or no, because “1,247 rows are wrong” and “some rows are wrong” lead to different decisions.
Some assertions are declared expected to fail. The public NYC files genuinely contain rows outside the month they are named for, and pretending otherwise would be the easy lie. What is watched is not whether they fail but whether they fail as declared — an assertion that suddenly starts passing usually means the data stopped arriving.
| Dataset | Assertion | Declared | Result | |
|---|---|---|---|---|
| demo.nyc_trips | fare-is-not-negative | expect fail | 92,152 violating rows | as declared |
| demo.nyc_trips | no-rows-outside-named-month | expect fail | 18 violating rows | as declared |
| demo.streamed_orders | consumed-after-emitted | expect pass | 0 violations | as declared |
| demo.streamed_orders | no-duplicate-offsets | expect pass | 0 violations | as declared |
| demo.synthetic_orders | event-time-within-a-day-of-load | expect fail | 400 violating rows | as declared |
| demo.synthetic_orders | every-order-has-a-status | expect pass | 0 violations | as declared |
| marts.trips_daily | one-row-per-day | expect pass | 0 violations | as declared |
| marts.trips_daily | trips-per-day-is-plausible | expect pass | 0 violations | as declared |
Table-level everywhere, column-level where it can be derived. The column edges are parsed out of the SQL that actually built the table — parsed, not pattern-matched. Ingestion has no manifest to read, so those edges are declared by a person and labelled as such rather than presented as if the system had worked them out.
| From | To | Grain | Origin |
|---|---|---|---|
| nyc-tlc-cdn:yellow_tripdata | demo.nyc_trips | table | declared |
| redpanda:orders | demo.streamed_orders | table | declared |
| data-weaver:orders | demo.synthetic_orders | table | declared |
| demo.nyc_trips | marts.trips_daily | table | dbt-manifest |
| demo.nyc_trips.fare | marts.trips_daily.avg_fare | column | dbt-manifest |
| demo.nyc_trips.distance_miles | marts.trips_daily.avg_miles | column | dbt-manifest |
| demo.nyc_trips.pickup | marts.trips_daily.day | column | dbt-manifest |
| demo.nyc_trips.total | marts.trips_daily.total_revenue | column | dbt-manifest |
| demo.nyc_trips.pickup_zone | marts.trips_daily.zones_used | column | dbt-manifest |
Read from the catalog, not from the policy file. The file says what should be true; this is what is. Where the two disagree, it appears in Findings above — and the finding that matters most is a privilege that exists and nothing declares. A missing grant breaks something and somebody complains within the hour; an extra one breaks nothing, complains to nobody, and is how access quietly accumulates.
| Principal | Can write? | Roles | Privileges |
|---|---|---|---|
| console | read-only | demo/reader | CATALOG_READ_PROPERTIES, NAMESPACE_LIST, NAMESPACE_READ_PROPERTIES, TABLE_LIST, TABLE_READ_DATA, TABLE_READ_PROPERTIES, VIEW_LIST, VIEW_READ_PROPERTIES |
| root | yes | demo/admin, demo/catalog_admin | CATALOG_MANAGE_ACCESS, CATALOG_MANAGE_CONTENT, CATALOG_MANAGE_METADATA |
The principal the public console runs as is the read-only one. That is enforced by the catalog, not by the query layer — asking Trino to write as that user is refused before the query engine is involved.
Not a diagram. Every row below was read from the running system when this page was generated: the containers that exist, the ports actually listening, and the data flow taken from lineage the platform derived for itself. If a component stops, it stops appearing here.
| Layer | Component | Running as | Reachable from |
|---|---|---|---|
| Ingest | event stream | redpandadata/redpanda:v26.2.2 | private network only |
| Store | object store | dxflrs/garage:v2.3.0 | private network only |
| Store | control-plane state | postgres:18 | private network only |
| Catalog | Iceberg REST catalog | apache/polaris:1.7.0 | private network only |
| Query | query engine | trinodb/trino:483 | private network only |
| Orchestrate | scheduler UI | platform-jobs:current | private network only |
| Orchestrate | scheduler daemon | platform-jobs:current | private network only |
| Control plane | registry, contracts, freshness | platform-jobs:current | private network only |
| Serve | bounded public query console | platform-jobs:current | the internet |
| Serve | this page | nginx:1.31.5-alpine | the internet |
Two of nine are reachable from the internet. The rest carry no routing label at all, which is what makes them unreachable — not a firewall rule somebody has to maintain.
A read-only SELECT against the demo catalog. This is the only part of the platform that takes input from strangers, and it has four independent limits in front of it: the statement is parsed and must be a single SELECT; Trino shows this user no catalog but demo; the catalog credential holds read privileges only; and a resource group caps it at two concurrent queries. Each assumes the others have failed.
This is early. The substrate is pinned and the ground is prepared, but the engines are not deployed, so there is no data, no lineage and nothing to measure freshness against. Those pages will appear when they have something real behind them, and not before — a freshness badge is read from a table snapshot or it does not exist.