AI-Generated UI With Real Data: Escaping the Terrarium
The AI-generated UI data binding problem is easy to miss, because the part that's broken is the part that looks finished. Ask Nozio to compose an app page and you get something genuinely convincing: cards laid out in columns, status labels color-coded, a summary across the top. The question that exposes it is mundane. Move a card to another column — does the underlying task document change?
We went looking for that answer in the code rather than in front of an audience, which is the only reason it wasn't embarrassing. A generated app page could bind to exactly one thing: its own private JSON blob, written by the same generation call that produced the layout. There was no path from the board to the task pages living in the workspace, and no amount of better prompting was going to create one.
Those pages were terrariums. They had the shape of a living system. The glass was clean. But everything inside was sealed off from the rest of the workspace. The tasks weren't real tasks. The statuses weren't pulling from any document store. The whole thing was running on sample JSON that the LLM had baked into the component spec when it generated the UI. Change a real document anywhere in the workspace and the board would never know. Write a new status to the board and it would evaporate on reload.
That gap clarified the actual problem we were building toward, and it's a harder problem than most people writing about AI-native tools are admitting right now.
The Terrarium Pattern in AI-Composed Interfaces
When an LLM generates a UI component, it reasons about structure and state at the same time. It produces a spec that describes both what the interface looks like and what data it operates on. The path of least resistance is to inline that data. The model knows what a kanban board looks like, it knows what plausible task data looks like, and it can produce a coherent artifact that satisfies the prompt without ever touching an external data source. The result is visually correct and functionally inert.
This is the terrarium pattern: generated UI that looks alive because it contains data, but the data is a prop, not a live feed.
In Nozio, the architecture made this especially visible. Every page in the system already had a rich property model. There was a per-database property schema, infinite dynamic properties per document, and full CRUD available through a consistent internal API. The workspace data layer was genuinely capable. But an AI-generated app page could only bind to its own private JSON blob. The generated component and the real document store were parallel universes that happened to render in the same browser window.
The gap wasn't a bug in the LLM's output. The gap was that nobody had built the bridge. And building that bridge turned out to require solving several distinct problems that are easy to conflate.
Why AI-Generated UI Data Binding Is Harder Than It Sounds
The obvious framing is: just give the LLM access to your data schema and let it generate the correct bindings. This sounds reasonable. It fails in practice for a few reasons that took us time to fully appreciate.
Schema complexity doesn't compress well into a prompt. Nozio's property system is expressive. A document can have typed properties, relation properties that point to other documents, rollup properties that aggregate across a relation, and formula properties that derive values from other properties. Describing all of that in a way that an LLM can reliably reason about, without hallucinating property names or misunderstanding cardinality, requires prompt engineering that is itself a significant engineering surface. The model needs to know not just what fields exist, but what operations are valid on each field type, what the write semantics are, and how the UI interaction maps to a mutation.
Generated bindings need to survive schema changes. Sample data is static. Real workspace data evolves. A property gets renamed. A relation gets restructured. A formula changes. If the generated UI is binding to a specific property name that it inferred from the schema at generation time, and that property name changes, the binding silently breaks. You need a binding layer that is schema-version-aware or that resolves by semantic intent rather than by raw field name.
Read and write are asymmetric problems. Getting generated UI to read real data is hard. Getting it to write back correctly is a different category of hard. A kanban board that reads from a status property is already interesting. A kanban board that writes a new status value when you drag a card, and does so through the correct mutation path for that property type, while respecting validation rules and triggering any downstream effects, is an integration problem that requires the generated component to understand the full semantics of the data layer, not just its shape.
The React documentation on controlled components is a useful frame here: the pattern of lifting state and passing callbacks down is well-understood in static UI. The challenge with AI-generated UI is that the component doesn't know at generation time which state is external and authoritative versus which state is local and ephemeral. The LLM has to make that decision, or the binding layer has to impose it after the fact.
What It Actually Took
Getting AI-generated app pages in Nozio to read and write actual workspace documents required solving the problem at three layers.
The first layer was the spec format. The generated UI spec had to be extended to express data bindings as first-class declarations, separate from the component structure and the sample data. Instead of a component that contained its data, we needed a component that declared its data dependencies in terms of workspace concepts: "this field binds to the status property of documents in this database." The sample data could still be present for preview rendering, but it had to be clearly marked as non-authoritative.
The second layer was the runtime binding engine. At render time, the binding declarations had to be resolved against the actual workspace document store. This meant the generated component spec was no longer self-contained. It was a description of intent that the runtime had to fulfill by querying real data, subscribing to real updates, and routing real mutations back through the correct write paths. The component became a view over live data rather than a container of static data.
The third layer was the LLM's understanding of what it was generating. This is the subtlest part. If the model doesn't understand that it's producing binding declarations rather than sample data, it will produce binding declarations that look like sample data. Property names will be plausible but invented. Database references will be well-formed but wrong. The model needs enough context about the actual workspace schema to produce bindings that resolve correctly at runtime. That means the schema, or a compressed representation of the relevant parts of it, has to be in the generation context. And that brings you back to the prompt engineering problem described above.
None of these layers is independently novel. Structured output support in modern LLM APIs makes it more tractable to get the model to produce a spec that separates structure from binding declarations. Runtime data binding systems have existed in frontend frameworks for years. Schema-aware code generation is a well-understood pattern in ORM tooling. The difficulty is that you have to solve all three simultaneously, and the failure modes at each layer compound each other.
The Seam Is the Product
Here's the thing I keep coming back to: in an AI-native document tool, the seam between generated UI and real data is not an implementation detail. It is the product.
If your generated dashboards and boards and views only work with sample data, you have built a prototyping tool. That's valuable, but it's a different category of product than a workspace where AI composes interfaces over your actual documents. The terrarium is a demo. The live data binding is the thing that makes someone's work actually move through the system.
This distinction matters more as AI-composed interfaces become more common. The pattern of generating plausible-looking UI that runs on fake data is everywhere right now. It's in AI design tools that produce mockups. It's in code generation tools that scaffold components with hardcoded arrays. It's in agent frameworks that demo beautifully against curated datasets and then struggle against real production data. The terrarium pattern is the default output of a system that optimizes for visual coherence without solving for data integration.
The teams that are going to build genuinely useful AI-native tools are the ones treating AI-generated UI data binding as a first-class engineering problem, not as something the LLM will figure out if you prompt it right. The LLM is one part of the solution. The binding runtime, the schema representation, the write semantics, the change propagation model: those are engineering decisions that have to be made explicitly and built deliberately.
What This Means for Full-Stack AI Integration
Working through this in Nozio changed how I think about the full-stack integration problem in AI-native apps more broadly.
The common framing is that AI integration is primarily a backend problem. You wire up an LLM, you manage context, you handle streaming responses. The frontend just renders what comes back. That framing is wrong for AI-composed UI. When the AI is generating the UI itself, the frontend architecture becomes the integration point. The component model, the state management approach, the data fetching layer: all of these have to be designed with the assumption that components will be generated at runtime and will need to bind to data sources that weren't known at build time.
That's a different set of constraints than building a frontend that calls an AI endpoint. It means your data access layer has to be introspectable at runtime in a way that a generated component can discover and use. It means your property schema has to be representable in a form that an LLM can reason about correctly. It means your mutation paths have to be self-describing enough that a generated binding can invoke them without having been compiled against them.
Tanstack Query's approach to query key factories is one example of a pattern that moves in the right direction: making data dependencies explicit and composable rather than scattered through component implementations. That kind of discipline in your data layer pays dividends when the components consuming that layer are being generated rather than hand-written.
The terrarium is comfortable to build. The demo always works. The data is always in the right shape because you put it there. Escaping the terrarium means accepting that the generated UI has to live in the same messy, evolving, user-owned data environment as everything else in the product. That's harder. It's also the only version that's actually useful.