rixmath.in — a data platform on one box

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.

Platform architecture Two sources feed batch and streaming ingestion into Iceberg tables catalogued by Polaris and stored in Garage, queried by Trino and transformed by dbt, all on one host under Dagster. A control plane reconciles declared datasets against the catalog and feeds this page; the only public door is Traefik, in front of the page and a bounded query console. ONE HOST · 8 CORES · 31.3 GiB · STORAGE, CATALOG AND QUERY CO-LOCATED SOURCES running Dagster schedules ingestion, dbt builds, reconciliation and weekly Iceberg maintenance NYC TLC trips public CDN, Parquet data-weaver synthetic, planted defects Batch ingest pyarrow, via catalog running Redpanda event stream, no ports Stream consumer one snapshot per run ICEBERG LAKEHOUSE running Polaris REST catalog + RBAC running Garage S3 object store running PostgreSQL catalog + control state running Trino one node, capped dbt-trino builds the marts running Query console SELECT only, 4 limits running This page rebuilt from state running Traefik the only public door — 2 routed containers VISITORS via Cloudflare CONTROL PLANE — BUILT HERE, EVERYTHING ABOVE IS ASSEMBLED reconciles declared vs actual Registry 5 datasets owner and purpose Contracts 1 broken schema vs Iceberg Freshness 4/5 in SLA snapshot age Quality 8 assertions 8 as declared Lineage 9 edges 5 column-level Policy 2 readers 1 read-only Ops box monitoring, logs, off-site backups — pulls Home lab bulk copies, restore drills — never in a request OFF THE HOST
container running nowdeclared but not runningno dot: a job, a library or a source, not a service

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.

The machine

Cores
8
x86_64
Memory free
25.6 GiB
of 31.3 GiB
Disk free
379 GB
of 415 GB
Uptime
4d 14h
load 0.57 0.35 0.32

Single host, single instance of everything. No high availability, and eight cores is the ceiling — this demonstrates design, not scale.

Declared versus actual

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.

ComponentChoicePinnedRunning
Object storeGarage2.3.0running
Table formatApache Iceberg1.11.0not a service
CatalogApache Polaris1.7.0running
Query engineTrino483running
Metadata storePostgreSQL18running
StreamingRedpanda26.2.2running
Transformdbt-trino1.10.3not a service
OrchestrationDagster1.13.21running
Control planeregistry, contracts, freshness—running
Query consolebounded public SQL—running

Running containers

NameImageState
openclaw-uy0y-openclaw-1ghcr.io/hostinger/hvps-openclaw:latestUp 28 hours
platform-consoleplatform-jobs:currentUp 4 days (healthy)
platform-controlplaneplatform-jobs:currentUp 4 days (healthy)
platform-dagsterplatform-jobs:currentUp 4 days (healthy)
platform-dagster-daemonplatform-jobs:currentUp 4 days
platform-garagedxflrs/garage:v2.3.0Up 4 days (healthy)
platform-polarisapache/polaris:1.7.0Up 4 days (healthy)
platform-postgrespostgres:18Up 4 days (healthy)
platform-redpandaredpandadata/redpanda:v26.2.2Up 4 days (healthy)
platform-sitenginx:1.31.5-alpineUp 4 days
platform-trinotrinodb/trino:483Up 4 days (healthy)
traefik-traefik-1traefik:v3.7.13Up 4 days

Storage layout

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.

PathSize
/data/garage/meta1.6M
/data/garage/data133M
/data/postgres72M
/data/polaris4.0K
/data/trino6.7M
/data/redpanda180M
/data/dagster1.5M
/data/backups3.9M
/srv/platform6.0M

Exposure

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.

PortProcessReachable from
80traefikthe internet
443traefikthe internet
—everything elseprivate network only (10 local listeners)

The control plane

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.

Datasets declared
5
Findings
2
2 breaking
Last reconciled
0.4h ago

Registry

Every dataset, and who is accountable for it. A dataset with no owner is a dataset nobody is going to fix.

DatasetOwnerDomainClassPurpose
demo.nyc_tripsrixonmobilitypublicPublic NYC TLC yellow taxi trip records, landed as Iceberg. The large, genuinely messy dataset -- partitioning, compaction and query planning matter h
demo.streamed_ordersrixonoperationssyntheticThe 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_ordersrixonoperationssyntheticA 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.tripsrixonplatformsyntheticThe 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_dailyrixonmobilitypublicDaily 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

Freshness

Each dataset declares how stale it is allowed to become. Late is a state, reported explicitly — not the absence of a green tick.

DatasetSLAState
demo.nyc_trips1080hmeeting-sla
demo.streamed_orders6hmeeting-sla
demo.synthetic_orders36hstalenewest snapshot is 1074.5h old; the declared SLA is 36h (1038.5h over)
demo.trips—static
marts.trips_daily1080hmeeting-sla

Storage

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.

DatasetRowsSnapshotsFilesAvg file
demo.nyc_trips9,308,8661914.1 MB
demo.streamed_orders92,000114636 KB
demo.synthetic_orders26,0001257 KB
demo.trips3121 KB
marts.trips_daily60113 KB

Findings

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.

SeverityDatasetFindingDetail
breakingdemo.synthetic_ordersstalenewest snapshot is 1074.5h old; the declared SLA is 36h (1038.5h over)
breakingdemo.synthetic_orderstype-mismatch'amount' is declared double but is actually varchar

Quality

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.

DatasetAssertionDeclaredResult
demo.nyc_tripsfare-is-not-negativeexpect fail92,152 violating rowsas declared
demo.nyc_tripsno-rows-outside-named-monthexpect fail18 violating rowsas declared
demo.streamed_ordersconsumed-after-emittedexpect pass0 violationsas declared
demo.streamed_ordersno-duplicate-offsetsexpect pass0 violationsas declared
demo.synthetic_ordersevent-time-within-a-day-of-loadexpect fail400 violating rowsas declared
demo.synthetic_ordersevery-order-has-a-statusexpect pass0 violationsas declared
marts.trips_dailyone-row-per-dayexpect pass0 violationsas declared
marts.trips_dailytrips-per-day-is-plausibleexpect pass0 violationsas declared

Lineage

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.

FromToGrainOrigin
nyc-tlc-cdn:yellow_tripdatademo.nyc_tripstabledeclared
redpanda:ordersdemo.streamed_orderstabledeclared
data-weaver:ordersdemo.synthetic_orderstabledeclared
demo.nyc_tripsmarts.trips_dailytabledbt-manifest
demo.nyc_trips.faremarts.trips_daily.avg_farecolumndbt-manifest
demo.nyc_trips.distance_milesmarts.trips_daily.avg_milescolumndbt-manifest
demo.nyc_trips.pickupmarts.trips_daily.daycolumndbt-manifest
demo.nyc_trips.totalmarts.trips_daily.total_revenuecolumndbt-manifest
demo.nyc_trips.pickup_zonemarts.trips_daily.zones_usedcolumndbt-manifest

Who may read what

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.

PrincipalCan write?RolesPrivileges
consoleread-onlydemo/readerCATALOG_READ_PROPERTIES, NAMESPACE_LIST, NAMESPACE_READ_PROPERTIES, TABLE_LIST, TABLE_READ_DATA, TABLE_READ_PROPERTIES, VIEW_LIST, VIEW_READ_PROPERTIES
rootyesdemo/admin, demo/catalog_adminCATALOG_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.

Architecture

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.

LayerComponentRunning asReachable from
Ingestevent streamredpandadata/redpanda:v26.2.2private network only
Storeobject storedxflrs/garage:v2.3.0private network only
Storecontrol-plane statepostgres:18private network only
CatalogIceberg REST catalogapache/polaris:1.7.0private network only
Queryquery enginetrinodb/trino:483private network only
Orchestratescheduler UIplatform-jobs:currentprivate network only
Orchestratescheduler daemonplatform-jobs:currentprivate network only
Control planeregistry, contracts, freshnessplatform-jobs:currentprivate network only
Servebounded public query consoleplatform-jobs:currentthe internet
Servethis pagenginx:1.31.5-alpinethe 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.

Query it yourself

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.

What is not here yet

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.