Architecture
The garden runs on this. Four layers, two engines, one governed dataset — click a surface to trace how it reaches the garden data and read the tradeoffs — or press Play to watch a row travel bronze → gold → surface → client.
Access matrix — which surface reads which source
| Access surface | Wire / protocol | Path it calls | Reaches | Source object | Governed by |
|---|---|---|---|---|---|
| Unity Catalog | REST (HTTPS) | /api/2.1/unity-catalog/* | metadata only | public catalog → bronze volume + gold tables | Unity Catalog |
| JDBC | Postgres wire (5432) | Lakebase Postgres garden.* | point read/write | garden.v_leaderboard · garden.v_plots (Postgres/OLTP) | Unity Catalog + Postgres roles |
| HTTP | REST (HTTPS) | /api/2.0/sql/statements | SQL execution | public.00_public_gold.garden_audit (Delta) | Unity Catalog |
| GraphQL | HTTP resolvers → both engines | /graphql | federated player read | public.00_public_gold.garden_audit (Delta) + garden.* (Postgres) | Unity Catalog + Postgres roles |
| Lakebase | Postgres wire (5432) | psql / pg / psycopg | point read/write | garden.players · garden.plots · garden.events | Unity Catalog + Postgres roles + OAuth |
Unity Catalog lakehouse governance
Mental model: UC is IAM + a data catalog combined — one place that both describes your data (catalog → schema → table → column) and controls who can touch it. Everything below is live UC REST metadata, the same thing every other surface is governed by.
Gaps & best practice
USE CATALOG on public + SELECT on gold only — private catalogs (e.g. dev_appian_poc) are genuinely blocked, not hidden.ALL PRIVILEGES to account users; grant to groups with least privilege; classify with tags; apply column masks + row filters; use on-behalf-of-user auth so each user's own grants apply.The OLTP schema Unity Catalog governs — Lakebase garden
This is the transactional half of the dataset: the Postgres garden schema behind the live game.
Unity Catalog governs it alongside the Delta lakehouse tables above — same discovery, lineage and permission plane, one dataset in two physical shapes.
How Unity Catalog governs Lakebase — especially from a Databricks App
garden.* tables below.psql / pg / GraphQL client connecting over the native wire authenticates with native Postgres roles, which live outside Unity Catalog. That path still works, but it's governed by Postgres role grants, not UC — which is exactly the governance-boundary point this POC makes.plots.state_id → ref_plot_states
plots.crop_id → ref_crops
plots.planted_by → players
events.plot_id → plots
events.player_id → players
events.action_id → ref_actions
events.crop_id → ref_crops
rabbits.plot_id → plots
Alternative Access Patterns Lakebase-first
How the app actually reaches its data. LTAP means most application access is transactional — point reads and writes against Lakebase (Postgres/OLTP). The lakehouse (Delta/OLAP) is the analytics surface you reach through the Statement API, and GraphQL is the one typed layer that federates both. Pick a surface below — every example is read-only and fixed (you run it; you can't edit the SQL that's sent).
JDBC Lakebase · Postgres wire
The workhorse driver path — but pointed where an app spends most of its time: the operational store.
A JDBC/ODBC client (BI tool, notebook, JVM app, ORM) holds a session against Lakebase Postgres and runs indexed,
millisecond point reads over the live garden schema. This is the OLTP half of LTAP.
SELECT name, plants, waters, harvests, score FROM garden.v_leaderboard ORDER BY score DESC LIMIT 10
garden.v_leaderboard / v_plots). The live game writes these tables transactionally as people garden.When to reach for JDBC
Tableau/Power BI, dbt, Spark, ORMs, any tool with a JDBC/ODBC slot. Against Lakebase you get true OLTP — indexes, transactions, sub-ms point reads. Session reuse keeps chatty app traffic cheap. Downside: needs a driver + a live connection.Tradeoffs & what UC governance you sacrifice
garden schema; map Databricks identities to Postgres roles via identity federation; manage synced tables through UC so lineage + access flow through one plane.Lakebase Postgres · OLTP
The operational system of record: managed Postgres 17, speaking the native wire protocol. Millisecond point
reads and transactional writes — what the lakehouse is deliberately bad at. Any Postgres client (psql, JDBC, pg,
ORMs) connects unchanged; auth is a short-lived Databricks OAuth token as the password (this app auto-refreshes it).
SELECT name, plants, waters, harvests, score FROM garden.v_leaderboard ORDER BY score DESC LIMIT 10
garden schema (players · plots · events). Read-only, fixed query; the live game writes these tables transactionally as people garden.When to reach for Lakebase
App state, user sessions, agent memory, feature serving, anything needing an index and a sub-ms write. Pair it with the lakehouse via synced tables so analytics and serving share one governed dataset.How Unity Catalog applies here
garden schema, map Databricks identities to Postgres roles via identity federation, and manage synced tables through UC so lineage + access flow through one plane.The transactional store doesn't live in isolation — below is its change feed (how writes reach the lakehouse and back) and its branching (fork the whole database in seconds). Same Lakebase, two more capabilities.
Change feed Delta CDF operational CDC
The two engines don't drift apart — changes flow between them. Lakehouse → Lakebase ships curated analytics down to the operational store for serving; Lakebase → Lakehouse streams operational writes up for analytics. Both are live below.
Lakehouse → Lakebase Delta CDF
Delta Change Data Feed exposes every insert/update/delete on the gold table as rows tagged with a
_change_type. A synced table (or a stream) reads that feed and upserts into Lakebase — no full reloads.
Lakebase → Lakehouse Lakebase CDF
lakebase_cdf
Operational writes (a player acting on a plot) are captured by native Lakebase Change Data Feed straight
off the Postgres write-ahead log — batched to Delta every ~15s as lb_<table>_history tables under the
lakebase_cdf catalog, each row tagged with _pg_change_type, LSN and commit timestamp. No triggers, no Debezium, no polling.
garden.events in Postgres — the exact insert Lakebase CDF captures off the WAL and lands in the lakehouse.How change capture works — Lakebase CDF
lb_<table>_history tables land in the lakebase_cdf catalog (external storage on R2) with full Unity Catalog lineage — grants and tags follow the data.LTAP — Lakehouse Transactional / Analytical Processing
LTAP runs your operational app and your analytics on one governed lakehouse. Two purpose-built engines — Lakebase (Postgres) for transactions, the Lakehouse (Delta) for analytics — sit under one platform, one governance plane (Unity Catalog), with zero-ETL sync so they behave like a single system. No separate warehouse to stand up, no nightly copy job, one set of permissions and lineage across both.
The problem: the two-system tax
What teams do today
You run an OLTP database for the app (users, sessions, orders, state) and a separate warehouse / lakehouse for analytics — glued together by brittle batch ETL. Two engines, two bills, and a pipeline in the middle you have to build and babysit.What that tax costs you
Copies drift — the warehouse is a stale snapshot of the app. Freshness lags the last ETL run. And you govern two systems with two policy models — separate permissions, separate lineage, separate audit — with nothing that sees the whole dataset.What LTAP collapses it into
The analogy
Forcing one database to do both is like one chef doing prep, plating and dishes — versatile but a bottleneck; bolting a warehouse on the side means nightly copies and stale data. LTAP is a proper line: a fast station for live plot actions (OLTP) and a batch/prep station for volume analytics (OLAP), sharing one kitchen and one manager — Unity Catalog.What makes it "one system"
Synced tables + Change Data Feed keep the two engines coherent with no hand-written ETL, and Unity Catalog governs both — so it reads like a single governed dataset with two physical shapes.When LTAP wins
You need real analytics (ML, big joins, BI) and sub-ms serving, independent scaling and cost per engine, and one lineage + permission plane across operational and analytical.The Difference: OLTP vs OLAP
Two workload types, two engine shapes. LTAP doesn't blur them — it runs a best-of-breed engine for each and governs both as one dataset. Below, the same table of garden events, read two different ways. Watch how each engine reaches into memory.
crop ever planted — to aggregate in seconds with Spark / Photon / ML / BI. Answers "what's true across all of it?" The Garden History requery and Genie both run here.Lakebase / row-store cost
Lakehouse / columnar cost
Why you historically needed BOTH — and what LTAP changes
Real World Use Cases
Where the two-system tax actually hurts — and how one governed dataset (Lakebase OLTP + lakehouse OLAP under Unity Catalog) removes it.
AI agent state & memory AI Realtime
Online feature serving for ML ML Realtime
Serving computed insights Data Near Real Time
Government case / permit management Gov/SLED Near Real Time
Fraud / anomaly detection FinServ Realtime
Operational app backend App Batch
The Garden History ad-hoc lakehouse query
No pre-synced mirror and no cached snapshot: Requery from lakehouse runs a fresh
SELECT over the captured change history through the SQL warehouse (round-trip latency shown), then the
slider replays those changes on the grid — reconstructing exactly what was planted where, one committed change at a
time. The pull is analytical (governed, columnar, seconds); the reconstruction happens in the browser.
Callouts and Considerations Free Edition & Databricks Apps
The honest ledger, ordered by what matters most. First the real architectural tradeoff you're making with LTAP, then why governance is the differentiator — and only then the Databricks Free Edition limitations and the workaround each one forced in this POC. On a paid workspace, the Free-Edition workarounds disappear; the tradeoff at the top is inherent to the pattern.
1 · The honest tradeoff — LTAP is not one ACID engine
2 · Governance is the differentiator
3 · Free Edition & Databricks Apps — limitations and workarounds
| Area | Full-featured Databricks | Limitation here | Workaround in this POC |
|---|---|---|---|
| App hosting | Run as a Databricks App — managed identity, OBO auth, resource bindings, secrets, autoscaling, no servers to run. | — | Go server on a VM behind nginx; it holds a service-principal token in env. |
| Realtime + game loop | Databricks Apps run a long-lived container — increasingly viable for persistent WebSocket connections + background loops, and Databricks is pushing near-real-time app patterns (Zerobus Ingest + Lakebase). | — | Now that the growth loop is in-process (a goroutine, not an external cron), the whole thing is one Go process — so it could deploy as a Databricks App. It runs on a VM here mostly by history, not necessity. |
| Lakebase → lakehouse CDC | Lakebase Change Data Feed captures Postgres changes straight off the write-ahead log and flushes to Delta — no triggers, no Debezium, no polling. | Lakebase CDF needs Postgres 17 + a UC destination with default storage disabled. | Both provisioned. Migrated the instance to PG17 (lakebase-poc17) and created a default-storage-disabled catalog lakebase_cdf (on R2 external storage), so native Lakebase CDF is now the Postgres→Delta path (lb_<table>_history Delta tables off the WAL). The old hand-rolled event-log replay is retired to a one-flag fallback (CDF_NATIVE). |
| Scale-to-zero & branching | Lakebase Autoscale scales compute to zero when idle and supports instant database branching. | Free Edition Lakebase is now on the Autoscale (Neon) compute model — a real production branch + native scale-to-zero — but the idle suspend is fixed at 24 h (the suspend_timeout edit is rejected on Free Edition), the branch/CU-tuning APIs aren't in the public SDK/CLI, and the legacy provisioned start/stop field is dead (rejected with min_cu == max_cu). | The garden's on/off button + a 5-min auto-off toggle the endpoint's spec.disabled flag via the /api/2.0/postgres API to reclaim credits well before the 24 h native suspend (the old stopped field is dead). An in-memory idle-gate also skips the growth tick after 5 min of no play, so it issues zero Lakebase + zero warehouse queries. With Lakebase CDF on, the feed needs the endpoint live — so the auto-off pauses CDF while idle and it resumes on wake (no writes happen while off, so nothing is missed); set LAKEBASE_ALWAYS_ON=1 for a continuous feed at the cost of steady credit use. The branching tree below is an interactive illustration. |
| Identity / auth | M2M OAuth service-principal credentials, and per-user OBO so UC enforces each user's grants. | Account-level OAuth (M2M) secrets aren't available on Free Edition. | An OBO token for a least-privilege service principal, scoped to catalog public. |
| Warehouse tuning | Set auto-stop, size and serverless scaling per workload. | Editing the SQL warehouse is blocked for free users. | Stuck at the 10-min default auto-stop — the idle-gate keeps it from being kept warm. |
| Governance hygiene | Least-privilege groups, row filters + column masks, no all-users grants. | Pre-existing catalogs are granted to account users, so any principal (incl. the SP) can see them. | The UC tab app-filters the catalog list to public — enforcement is real at the query layer, the filter is just presentation. |
| Sync & pipelines | Synced tables (declarative reverse-ETL) and Lakeflow managed pipelines move data both ways, UC-governed. | Synced tables (Delta→Lakebase) not exercised on Free Edition. | Postgres→Delta now runs on native Lakebase CDF (not app code). Delta→Lakebase is still illustrated rather than run as a managed synced table. |
| Genie / AI-BI | Genie natural-language dashboards over UC tables answer questions about both engines. | — | Built — the "Ask the Garden — Genie" panel on the Garden page runs a real Genie space over the gold garden tables (garden_audit). Lakebase data reaches it via synced tables. |
| JDBC driver | The Go SQL driver databricks-sql-go (Thrift + Arrow) for the JDBC path. | — | The Go backend routes the JDBC surface through the HTTP Statement Execution API to keep the build lean. |